diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 897d5fdf..3f737a8f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -31,6 +31,13 @@
android:theme="@style/Theme.NetBird"
tools:targetApi="31">
+
+
+
+
+
+
+ - Allow only listed apps (everything else bypasses)
+ - Disallow listed apps (everything else routes)
+
+
+
+ - allow
+ - disallow
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5fb4474b..fead681a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -148,4 +148,58 @@
Switched to profile \'%s\'
Logged out from profile \'%s\'
Profile \'%s\' removed successfully
+
+
+ Management URL
+ URL of the NetBird management server. Format https://host[:port].
+
+ Pre-shared key
+ WireGuard pre-shared key used as an additional symmetric secret. Secret value.
+
+ Disable auto-connect
+ When enabled, the tunnel does not auto-connect at app start.
+
+ Disable client routes
+ When enabled, this client does not consume routes advertised by routing peers.
+
+ Disable server routes
+ When enabled, this client does not act as a routing peer for other clients.
+
+ Block inbound
+ When enabled, the client blocks all inbound peer traffic on the WireGuard interface.
+
+ Allow server SSH
+ When enabled, this client accepts incoming SSH sessions via NetBird SSH.
+
+ Enable Rosenpass
+ Enables Rosenpass post-quantum key exchange on WireGuard tunnels.
+
+ Rosenpass permissive
+ When enabled, falls back to plain WireGuard if a peer does not support Rosenpass.
+
+ WireGuard port
+ UDP port for the local WireGuard interface. Allowed range 1-65535.
+
+ Split tunnel mode
+ Choose allow (only listed apps route through NetBird) or disallow (listed apps bypass NetBird).
+
+ Split tunnel apps
+ Comma-separated list of package names used by the selected split-tunnel mode.
+
+ Disable update settings
+ When enabled, blocks every configuration change from the UI and CLI.
+
+ Disable profiles
+ When enabled, the client cannot list, create, switch or remove NetBird connection profiles.
+
+ Disable networks
+ When enabled, the client UI cannot list, select or deselect NetBird networks.
+
+ Disable advanced view
+ When enabled, the new UI hides the advanced-view section.
+
+ Disable metrics collection
+ When enabled, the client does not collect or report local usage metrics.
diff --git a/app/src/main/res/xml/app_restrictions.xml b/app/src/main/res/xml/app_restrictions.xml
new file mode 100644
index 00000000..08462ff6
--- /dev/null
+++ b/app/src/main/res/xml/app_restrictions.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/netbird b/netbird
index 07e54501..b2c57328 160000
--- a/netbird
+++ b/netbird
@@ -1 +1 @@
-Subproject commit 07e5450117dd0451aaeefc18729a822115587e69
+Subproject commit b2c5732847ae5c22ab8f526204d9f121df72869e
diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java b/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java
index ecebc474..28c5fe03 100644
--- a/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java
+++ b/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java
@@ -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);
+ }
+ }
+
/**
* Cancels any pending debounced restart. Called whenever an external
* actor (typically a user-driven Connect/Disconnect) takes over the
diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java
index d4069b51..da309ffb 100644
--- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java
+++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java
@@ -48,6 +48,12 @@ public EngineRunner(Context context, NetworkChangeListener networkChangeListener
iFaceDiscover,
networkChangeListener);
+ // Per-Client MDM policy fetcher (DI on the goClient side).
+ // The Go layer holds the *mdm.Loader on this Client instance;
+ // every Run/RunWithoutLogin call overlays the latest MDM
+ // policy on top of the freshly resolved Config.
+ goClient.setMDMPolicyFetcher(new MDMPolicyFetcher(context));
+
updateLogLevel(isTraceLogEnabled, isDebuggable);
}
diff --git a/tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java b/tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java
new file mode 100644
index 00000000..09fc8536
--- /dev/null
+++ b/tool/src/main/java/io/netbird/client/tool/MDMPolicyFetcher.java
@@ -0,0 +1,72 @@
+package io.netbird.client.tool;
+
+import android.content.Context;
+import android.content.RestrictionsManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import io.netbird.gomobile.android.PolicyFetcher;
+
+/**
+ * MDMPolicyFetcher reads the current Android managed-config snapshot
+ * from RestrictionsManager and returns it as a JSON-encoded string to
+ * the Go layer. Registered on the goClient via setMDMPolicyFetcher
+ * inside EngineRunner; the Go side invokes fetchJSON() on every
+ * Loader.Load call so the response is always fresh.
+ *
+ * Returns an empty string when no managed config is set — the daemon
+ * side treats that as the "no MDM source present" sentinel.
+ *
+ * Lives in the tool package so the network-extension target can
+ * instantiate it without depending on the app package.
+ */
+public class MDMPolicyFetcher implements PolicyFetcher {
+ private static final String TAG = "MDMPolicyFetcher";
+
+ private final Context context;
+
+ public MDMPolicyFetcher(Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ @Override
+ public String fetchJSON() {
+ RestrictionsManager rm = (RestrictionsManager) context.getSystemService(Context.RESTRICTIONS_SERVICE);
+ if (rm == null) {
+ return "";
+ }
+ Bundle restrictions = rm.getApplicationRestrictions();
+ if (restrictions == null || restrictions.isEmpty()) {
+ return "";
+ }
+ try {
+ return bundleToJSON(restrictions).toString();
+ } catch (JSONException e) {
+ Log.w(TAG, "Failed to serialize managed restrictions to JSON: " + e);
+ return "";
+ }
+ }
+
+ 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);
+ }
+ obj.put(key, arr);
+ } else {
+ obj.put(key, value);
+ }
+ }
+ return obj;
+ }
+}
diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java
index 83ab6124..f938fcae 100644
--- a/tool/src/main/java/io/netbird/client/tool/VPNService.java
+++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java
@@ -39,6 +39,7 @@ public class VPNService extends android.net.VpnService {
private ConcreteNetworkAvailabilityListener networkAvailabilityListener;
private EngineRestarter engineRestarter;
private android.content.BroadcastReceiver stopEngineReceiver;
+ private android.content.BroadcastReceiver mdmPolicyChangedReceiver;
@Override
public void onCreate() {
@@ -99,6 +100,37 @@ public void onReceive(Context context, Intent intent) {
filter,
Context.RECEIVER_NOT_EXPORTED
);
+
+ // Listen for MDM managed-config changes. The OS broadcasts this
+ // intent whenever a Device Owner / Profile Owner pushes new
+ // application restrictions; the receiver only signals the engine
+ // to restart with a freshly resolved Config — the actual values
+ // are read on demand by MDMPolicyFetcher.fetchJSON during the
+ // next Run.
+ mdmPolicyChangedReceiver = new android.content.BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED.equals(intent.getAction())) {
+ Log.d(LOGTAG, "Received MDM policy change broadcast");
+ // Route through EngineRestarter so the existing restart
+ // machinery (state-listener gating, UI suppression
+ // window, runWithoutAuth on stop) handles the stop +
+ // re-run cleanly. A bare engineRunner.stop() leaves the
+ // engine stopped with no one to relaunch it.
+ if (engineRestarter != null) {
+ engineRestarter.requestRestartNow();
+ }
+ }
+ }
+ };
+ android.content.IntentFilter mdmFilter = new android.content.IntentFilter(
+ Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
+ androidx.core.content.ContextCompat.registerReceiver(
+ this,
+ mdmPolicyChangedReceiver,
+ mdmFilter,
+ Context.RECEIVER_NOT_EXPORTED
+ );
}
@Override
@@ -147,6 +179,13 @@ public void onDestroy() {
Log.w(LOGTAG, "Receiver not registered", e);
}
}
+ if (mdmPolicyChangedReceiver != null) {
+ try {
+ unregisterReceiver(mdmPolicyChangedReceiver);
+ } catch (IllegalArgumentException e) {
+ Log.w(LOGTAG, "MDM receiver not registered", e);
+ }
+ }
networkAvailabilityListener.unsubscribe();
networkChangeDetector.unsubscribe();