-
Notifications
You must be signed in to change notification settings - Fork 107
Mdm integration #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Mdm integration #198
Changes from all commits
456b361
de54bf4
b272d9f
8469ab4
f4d48a6
f41075d
f9919b5
0b62139
7f64edc
8442624
cee5fa9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,13 @@ | ||
| package io.netbird.client.ui.advanced; | ||
|
|
||
| import android.content.Context; | ||
| import android.content.RestrictionsManager; | ||
| import android.content.SharedPreferences; | ||
| import android.os.Bundle; | ||
| import android.util.Log; | ||
| import android.widget.CompoundButton; | ||
| import android.widget.EditText; | ||
| import android.view.View; | ||
| import android.view.LayoutInflater; | ||
| import android.view.View; | ||
| import android.view.ViewGroup; | ||
|
|
@@ -292,11 +296,79 @@ private void initializeEngineConfigSwitches() { | |
| binding.switchDisableIpv6.toggle(); | ||
| }); | ||
|
|
||
| applyMDMLocks(); | ||
|
|
||
| } catch (Exception e) { | ||
| Log.e(LOGTAG, "Failed to initialize engine config switches", e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Lock and align every UI control whose corresponding key is currently | ||
| * MDM-enforced. The list of managed keys + their enforced values is | ||
| * read directly from RestrictionsManager — the same OS-native source | ||
| * the Go layer uses (via MDMPolicyFetcher). No round-trip to Go is | ||
| * needed, and the two sides cannot diverge. | ||
| * | ||
| * For each managed key: | ||
| * - the switch is forced to the MDM value (overrides the user's | ||
| * on-disk preference); | ||
| * - the switch + its surrounding clickable layout are disabled so | ||
| * the user cannot toggle them. | ||
| */ | ||
| private void applyMDMLocks() { | ||
| Context ctx = getContext(); | ||
| if (ctx == null) { | ||
| return; | ||
| } | ||
| RestrictionsManager rm = (RestrictionsManager) ctx.getSystemService(Context.RESTRICTIONS_SERVICE); | ||
| if (rm == null) { | ||
| return; | ||
| } | ||
| android.os.Bundle restrictions = rm.getApplicationRestrictions(); | ||
| if (restrictions == null || restrictions.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| lockSwitchIfManaged(restrictions, "rosenpassEnabled", binding.switchRosenpass, binding.layoutRosenpas); | ||
| lockSwitchIfManaged(restrictions, "rosenpassPermissive", binding.switchRosenpassPermissive, binding.layoutRosenpassPermissive); | ||
| lockSwitchIfManaged(restrictions, "allowServerSSH", binding.switchAllowSsh, binding.layoutAllowSsh); | ||
| lockSwitchIfManaged(restrictions, "blockInbound", binding.switchBlockInbound, binding.layoutBlockInbound); | ||
| lockSwitchIfManaged(restrictions, "disableClientRoutes", binding.switchDisableClientRoutes, binding.layoutDisableClientRoutes); | ||
| lockSwitchIfManaged(restrictions, "disableServerRoutes", binding.switchDisableServerRoutes, binding.layoutDisableServerRoutes); | ||
|
|
||
| // PreSharedKey is a string, not a bool; lock the field if managed. | ||
| if (restrictions.containsKey("preSharedKey")) { | ||
| EditText psk = binding.presharedKey; | ||
| psk.setEnabled(false); | ||
| // Show the redaction sentinel so the actual MDM value is never | ||
| // leaked into the UI — matches the daemon-side behavior of | ||
| // GetConfig. | ||
| psk.setText(hiddenKey); | ||
| binding.btnSave.setEnabled(false); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Helper: if `key` is present in the OS-pushed restrictions, force the | ||
| * switch to its enforced bool value and disable the switch and its | ||
| * parent layout. The parent layout must be disabled too, otherwise | ||
| * the TV-remote "tap layout to toggle switch" path remains active. | ||
| */ | ||
| 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); | ||
| } | ||
| } | ||
|
Comment on lines
+358
to
+370
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Listener side-effects fire during MDM lock application.
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 🤖 Prompt for AI Agents |
||
|
|
||
| @Override | ||
| public void onDestroyView() { | ||
| super.onDestroyView(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <!-- Display labels for the splitTunnelMode managed restriction. --> | ||
| <string-array name="restriction_splitTunnelMode_entries"> | ||
| <item>Allow only listed apps (everything else bypasses)</item> | ||
| <item>Disallow listed apps (everything else routes)</item> | ||
| </string-array> | ||
| <!-- Raw values written into RestrictionsManager for splitTunnelMode. --> | ||
| <string-array name="restriction_splitTunnelMode_values"> | ||
| <item>allow</item> | ||
| <item>disallow</item> | ||
| </string-array> | ||
| </resources> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <!-- | ||
| Managed-configuration schema for the NetBird Android client. | ||
| Read by Device Owner / Profile Owner (Intune, MobileIron, Workspace ONE, | ||
| JumpCloud, TestDPC) to render a configuration UI; pushed values land in | ||
| RestrictionsManager.getApplicationRestrictions() and are surfaced to the | ||
| Go layer via MDMPolicyFetcher.fetchJSON(). | ||
|
|
||
| Key names mirror the canonical mdm.Key* constants in | ||
| client/mdm/policy.go (lowerCamelCase). Adding a key here is purely | ||
| discovery for the MDM admin UI — the Go side determines which keys | ||
| actually take effect. | ||
| --> | ||
| <restrictions xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| <restriction | ||
| android:key="managementURL" | ||
| android:title="@string/restriction_managementURL_title" | ||
| android:description="@string/restriction_managementURL_description" | ||
| android:restrictionType="string" | ||
| android:defaultValue="https://api.netbird.io:443" /> | ||
|
|
||
| <restriction | ||
| android:key="preSharedKey" | ||
| android:title="@string/restriction_preSharedKey_title" | ||
| android:description="@string/restriction_preSharedKey_description" | ||
| android:restrictionType="string" /> | ||
|
|
||
| <restriction | ||
| android:key="disableAutoConnect" | ||
| android:title="@string/restriction_disableAutoConnect_title" | ||
| android:description="@string/restriction_disableAutoConnect_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="disableClientRoutes" | ||
| android:title="@string/restriction_disableClientRoutes_title" | ||
| android:description="@string/restriction_disableClientRoutes_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="disableServerRoutes" | ||
| android:title="@string/restriction_disableServerRoutes_title" | ||
| android:description="@string/restriction_disableServerRoutes_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="blockInbound" | ||
| android:title="@string/restriction_blockInbound_title" | ||
| android:description="@string/restriction_blockInbound_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="allowServerSSH" | ||
| android:title="@string/restriction_allowServerSSH_title" | ||
| android:description="@string/restriction_allowServerSSH_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="rosenpassEnabled" | ||
| android:title="@string/restriction_rosenpassEnabled_title" | ||
| android:description="@string/restriction_rosenpassEnabled_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="rosenpassPermissive" | ||
| android:title="@string/restriction_rosenpassPermissive_title" | ||
| android:description="@string/restriction_rosenpassPermissive_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="wireguardPort" | ||
| android:title="@string/restriction_wireguardPort_title" | ||
| android:description="@string/restriction_wireguardPort_description" | ||
| android:restrictionType="integer" | ||
| android:defaultValue="51820" /> | ||
|
|
||
| <restriction | ||
| android:key="splitTunnelMode" | ||
| android:title="@string/restriction_splitTunnelMode_title" | ||
| android:description="@string/restriction_splitTunnelMode_description" | ||
| android:restrictionType="choice" | ||
| android:entries="@array/restriction_splitTunnelMode_entries" | ||
| android:entryValues="@array/restriction_splitTunnelMode_values" | ||
| android:defaultValue="allow" /> | ||
|
|
||
| <restriction | ||
| android:key="splitTunnelApps" | ||
| android:title="@string/restriction_splitTunnelApps_title" | ||
| android:description="@string/restriction_splitTunnelApps_description" | ||
| android:restrictionType="string" /> | ||
|
|
||
| <restriction | ||
| android:key="disableUpdateSettings" | ||
| android:title="@string/restriction_disableUpdateSettings_title" | ||
| android:description="@string/restriction_disableUpdateSettings_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="disableProfiles" | ||
| android:title="@string/restriction_disableProfiles_title" | ||
| android:description="@string/restriction_disableProfiles_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="disableNetworks" | ||
| android:title="@string/restriction_disableNetworks_title" | ||
| android:description="@string/restriction_disableNetworks_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="disableAdvancedView" | ||
| android:title="@string/restriction_disableAdvancedView_title" | ||
| android:description="@string/restriction_disableAdvancedView_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| <restriction | ||
| android:key="disableMetricsCollection" | ||
| android:title="@string/restriction_disableMetricsCollection_title" | ||
| android:description="@string/restriction_disableMetricsCollection_description" | ||
| android:restrictionType="bool" | ||
| android:defaultValue="false" /> | ||
|
|
||
| </restrictions> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -308,6 +308,22 @@ public void onNetworkTypeChanged() { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Triggers the same stop + restart sequence used by the network-change | ||
| * path, but without the debounce delay. Used by the MDM policy-change | ||
| * broadcast receiver: when an admin pushes a new managed config the | ||
| * engine must restart immediately so the new values take effect on the | ||
| * next Run (which re-reads MDM via MDMPolicyFetcher). | ||
| */ | ||
| public void requestRestartNow() { | ||
| Log.d(LOGTAG, "explicit restart requested (no debounce)"); | ||
| synchronized (restartLock) { | ||
| restartScheduled = true; | ||
| handler.removeCallbacks(restartRunnable); | ||
| handler.post(restartRunnable); | ||
| } | ||
| } | ||
|
Comment on lines
+318
to
+325
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Immediate MDM restart requests can be dropped during an in-flight restart. If 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 |
||
|
|
||
| /** | ||
| * Cancels any pending debounced restart. Called whenever an external | ||
| * actor (typically a user-driven Connect/Disconnect) takes over the | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents