diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 897d5fdf..c2a38614 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,6 +31,10 @@ android:theme="@style/Theme.NetBird" tools:targetApi="31"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d6b1f51b..ec73533f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -145,4 +145,20 @@ Switched to profile \'%s\' Logged out from profile \'%s\' Profile \'%s\' removed successfully + + + Management URL + NetBird management server URL (e.g., https://api.netbird.io:443) + Setup Key + Setup key for automatic device registration. Used once during initial enrollment. + Admin URL + NetBird admin dashboard URL (e.g., https://app.netbird.io:443) + Pre-Shared Key + WireGuard pre-shared key for additional encryption layer + Enable Rosenpass + Enable Rosenpass post-quantum encryption + Rosenpass Permissive Mode + Allow connections to peers that do not support Rosenpass + Disable Auto-Connect + Prevent the VPN from automatically connecting on app launch 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..8ef73071 --- /dev/null +++ b/app/src/main/res/xml/app_restrictions.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + 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 dfdbf846..f3e4ddfd 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -23,6 +23,12 @@ class EngineRunner { private static final String LOGTAG = "EngineRunner"; + + /** + * Lock to serialise MDM config apply operations between EngineRunner.runClient() + * and ManagedConfigReceiver.onReceive(). + */ + static final Object MDM_CONFIG_LOCK = new Object(); private final Context context; private final boolean isDebuggable; private final ProfileManagerWrapper profileManager; @@ -94,6 +100,43 @@ private synchronized void runClient(@Nullable URLOpener urlOpener, boolean isAnd var platformFiles = new AndroidPlatformFiles(configurationFilePath, stateFilePath, context.getCacheDir().getAbsolutePath()); Log.d(LOGTAG, "Running engine with config: " + configurationFilePath + ", state: " + stateFilePath); + // Apply MDM managed configuration before starting the engine. + // MDM values override user-set preferences on every launch. + try { + synchronized (MDM_CONFIG_LOCK) { + io.netbird.gomobile.android.ManagedConfig mdmConfig = ManagedConfigReader.read(context); + if (mdmConfig != null && mdmConfig.hasConfig()) { + mdmConfig.apply(configurationFilePath); + Log.i(LOGTAG, "MDM managed configuration applied"); + + // If MDM provides a setup key, attempt silent registration. + // loginWithSetupKeySync will call the management server which + // returns success (already registered) or registers the peer. + // The server is the authority — no local enrollment flag needed. + if (mdmConfig.hasSetupKey()) { + try { + io.netbird.gomobile.android.Auth auth = + io.netbird.gomobile.android.Android.newAuth( + configurationFilePath, + mdmConfig.getManagementURL()); + if (auth != null) { + auth.loginWithSetupKeySync( + mdmConfig.getSetupKey(), + DeviceName.getDeviceName()); + Log.i(LOGTAG, "MDM: silent setup key registration completed"); + } + } catch (Exception e) { + // Setup key login may fail if already registered or key expired. + // This is not fatal — continue with normal flow. + Log.w(LOGTAG, "MDM: setup key login skipped or failed: " + e.getMessage()); + } + } + } + } + } catch (Exception e) { + Log.e(LOGTAG, "Failed to apply MDM config, continuing with existing config", e); + } + try { notifyServiceStateListeners(true); if (urlOpener == null) { diff --git a/tool/src/main/java/io/netbird/client/tool/ManagedConfigReader.java b/tool/src/main/java/io/netbird/client/tool/ManagedConfigReader.java new file mode 100644 index 00000000..044efd3a --- /dev/null +++ b/tool/src/main/java/io/netbird/client/tool/ManagedConfigReader.java @@ -0,0 +1,120 @@ +package io.netbird.client.tool; + +import android.content.Context; +import android.content.RestrictionsManager; +import android.os.Bundle; +import android.util.Log; + +import io.netbird.gomobile.android.Android; +import io.netbird.gomobile.android.ManagedConfig; + +/** + * Reads MDM-managed configuration from Android Enterprise managed configurations + * (app restrictions). Configuration is pushed by EMM/MDM solutions such as + * Microsoft Intune, VMware Workspace ONE, or Google Admin Console. + * + *

The key names match those defined in res/xml/app_restrictions.xml and the + * Go SDK's ManagedConfig key constants.

+ */ +public class ManagedConfigReader { + + private static final String TAG = "ManagedConfigReader"; + + private ManagedConfigReader() { + // utility class + } + + /** + * Reads managed configuration from RestrictionsManager and returns a populated + * ManagedConfig instance. Returns null if no managed configuration is available + * or the RestrictionsManager service is unavailable. + * + * @param context Android context + * @return populated ManagedConfig, or null if no MDM config is present + */ + public static ManagedConfig read(Context context) { + RestrictionsManager restrictionsManager = + (RestrictionsManager) context.getSystemService(Context.RESTRICTIONS_SERVICE); + if (restrictionsManager == null) { + Log.d(TAG, "RestrictionsManager not available"); + return null; + } + + Bundle restrictions = restrictionsManager.getApplicationRestrictions(); + if (restrictions == null || restrictions.isEmpty()) { + Log.d(TAG, "No managed configuration found"); + return null; + } + + ManagedConfig config = Android.newManagedConfig(); + + String managementUrl = restrictions.getString( + Android.getManagedConfigKeyManagementURL(), ""); + if (!managementUrl.isEmpty()) { + config.setManagementURL(managementUrl); + Log.i(TAG, "MDM: management URL configured"); + } + + String setupKey = restrictions.getString( + Android.getManagedConfigKeySetupKey(), ""); + if (!setupKey.isEmpty()) { + config.setSetupKey(setupKey); + // Do not log the setup key value for security + Log.i(TAG, "MDM: setup key configured"); + } + + String adminUrl = restrictions.getString( + Android.getManagedConfigKeyAdminURL(), ""); + if (!adminUrl.isEmpty()) { + config.setAdminURL(adminUrl); + Log.i(TAG, "MDM: admin URL configured"); + } + + String preSharedKey = restrictions.getString( + Android.getManagedConfigKeyPreSharedKey(), ""); + if (!preSharedKey.isEmpty()) { + config.setPreSharedKey(preSharedKey); + Log.i(TAG, "MDM: pre-shared key configured"); + } + + if (restrictions.containsKey(Android.getManagedConfigKeyRosenpassEnabled())) { + boolean rosenpassEnabled = restrictions.getBoolean( + Android.getManagedConfigKeyRosenpassEnabled(), false); + config.setRosenpassEnabled(rosenpassEnabled); + Log.i(TAG, "MDM: Rosenpass enabled=" + rosenpassEnabled); + } + + if (restrictions.containsKey(Android.getManagedConfigKeyRosenpassPermissive())) { + boolean rosenpassPermissive = restrictions.getBoolean( + Android.getManagedConfigKeyRosenpassPermissive(), false); + config.setRosenpassPermissive(rosenpassPermissive); + Log.i(TAG, "MDM: Rosenpass permissive=" + rosenpassPermissive); + } + + if (restrictions.containsKey(Android.getManagedConfigKeyDisableAutoConnect())) { + boolean disableAutoConnect = restrictions.getBoolean( + Android.getManagedConfigKeyDisableAutoConnect(), false); + config.setDisableAutoConnect(disableAutoConnect); + Log.i(TAG, "MDM: disable auto-connect=" + disableAutoConnect); + } + + if (!config.hasConfig()) { + Log.d(TAG, "MDM restrictions present but no NetBird keys configured"); + return null; + } + + Log.i(TAG, "MDM managed configuration loaded successfully"); + return config; + } + + /** + * Returns true if any MDM-managed configuration is available for this app. + * + * @param context Android context + * @return true if managed config has values + */ + public static boolean hasManagedConfig(Context context) { + ManagedConfig config = read(context); + return config != null && config.hasConfig(); + } +} diff --git a/tool/src/main/java/io/netbird/client/tool/ManagedConfigReceiver.java b/tool/src/main/java/io/netbird/client/tool/ManagedConfigReceiver.java new file mode 100644 index 00000000..5d3ffaae --- /dev/null +++ b/tool/src/main/java/io/netbird/client/tool/ManagedConfigReceiver.java @@ -0,0 +1,58 @@ +package io.netbird.client.tool; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import io.netbird.gomobile.android.ManagedConfig; + +/** + * Receives {@code Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED} broadcasts + * when the MDM/EMM pushes updated managed configuration to the device. + * + *

This receiver re-reads the managed configuration and applies it to the + * Go SDK config file. Work is performed off the main thread via {@code goAsync()} + * to avoid ANRs.

+ * + *

Register this receiver in AndroidManifest.xml or dynamically in the VPNService.

+ */ +public class ManagedConfigReceiver extends BroadcastReceiver { + + private static final String TAG = "ManagedConfigReceiver"; + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); + + @Override + public void onReceive(Context context, Intent intent) { + if (!Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED.equals(intent.getAction())) { + return; + } + + Log.i(TAG, "Application restrictions changed, re-reading MDM config"); + + final PendingResult pendingResult = goAsync(); + EXECUTOR.execute(() -> { + try { + synchronized (EngineRunner.MDM_CONFIG_LOCK) { + ManagedConfig config = ManagedConfigReader.read(context); + if (config == null || !config.hasConfig()) { + Log.d(TAG, "No MDM config after restrictions change"); + return; + } + + ProfileManagerWrapper profileManager = new ProfileManagerWrapper(context); + String configPath = profileManager.getActiveConfigPath(); + config.apply(configPath); + Log.i(TAG, "MDM config re-applied after restrictions change"); + } + } catch (Exception e) { + Log.e(TAG, "Failed to apply MDM config after restrictions change", e); + } finally { + pendingResult.finish(); + } + }); + } +}