@@ -44,8 +44,8 @@ import java.io.IOException
4444import java.lang.UnsupportedOperationException
4545import java.net.NetworkInterface
4646import java.security.GeneralSecurityException
47- import java.util.Locale
4847import java.util.Collections
48+ import java.util.Locale
4949import kotlinx.coroutines.CoroutineScope
5050import kotlinx.coroutines.Dispatchers
5151import kotlinx.coroutines.SupervisorJob
@@ -54,11 +54,11 @@ import kotlinx.coroutines.flow.combine
5454import kotlinx.coroutines.flow.distinctUntilChanged
5555import kotlinx.coroutines.flow.first
5656import kotlinx.coroutines.launch
57+ import kotlinx.serialization.Serializable
5758import kotlinx.serialization.encodeToString
5859import kotlinx.serialization.json.Json
59- import kotlinx.serialization.encodeToString
60- import kotlinx.serialization.Serializable
6160import libtailscale.Libtailscale
61+
6262class App : UninitializedApp (), libtailscale.AppContext, ViewModelStoreOwner {
6363 val applicationScope = CoroutineScope (SupervisorJob () + Dispatchers .Default )
6464
@@ -154,7 +154,16 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
154154 // Check if a directory URI has already been stored.
155155 val storedUri = getStoredDirectoryUri()
156156 val rm = getSystemService(Context .RESTRICTIONS_SERVICE ) as RestrictionsManager
157- val hardwareAttestation = rm.applicationRestrictions.getBoolean(MDMSettings .KEY_HARDWARE_ATTESTATION , true )
157+ val hardwareAttestation =
158+ rm.applicationRestrictions.getBoolean(MDMSettings .KEY_HARDWARE_ATTESTATION , true )
159+
160+ // Populate MDM settings before starting Tailscale so that the rsop
161+ // policy framework reads correct values during its initial synchronous load
162+ // in newPolicy(). If MDM settings are not populated, rsop caches "not configured"
163+ // for Hostname and only corrects it after policyReloadMinDelay,
164+ // at which point the device may have already registered with the wrong hostname.
165+ MDMSettings .loadFrom(lazy { getEncryptedPrefs() }, rm)
166+
158167 if (storedUri != null && storedUri.toString().startsWith(" content://" )) {
159168 startLibtailscale(storedUri.toString(), hardwareAttestation)
160169 } else {
@@ -167,6 +176,8 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
167176 applicationScope.launch {
168177 val rm = getSystemService(Context .RESTRICTIONS_SERVICE ) as RestrictionsManager
169178 MDMSettings .update(get(), rm)
179+ }
180+ applicationScope.launch {
170181 Notifier .state.collect { _ ->
171182 combine(Notifier .state, MDMSettings .forceEnabled.flow, Notifier .prefs, Notifier .netmap) {
172183 state,
@@ -284,8 +295,11 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
284295
285296 override fun getDeviceName (): String {
286297 // Try user-defined device name first
287- android.provider.Settings .Global .getString(contentResolver, android.provider.Settings .Global .DEVICE_NAME )
288- ?.let { return it }
298+ android.provider.Settings .Global .getString(
299+ contentResolver, android.provider.Settings .Global .DEVICE_NAME )
300+ ?.let {
301+ return it
302+ }
289303
290304 // Otherwise fallback to manufacturer + model
291305 val manu = Build .MANUFACTURER
@@ -306,26 +320,27 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
306320
307321 @Serializable
308322 data class AddrJson (
309- val ip : String ,
310- val prefixLen : Int ,
323+ val ip : String ,
324+ val prefixLen : Int ,
311325 )
312-
326+
313327 @Serializable
314328 data class InterfaceJson (
315- val name : String ,
316- val index : Int ,
317- val mtu : Int ,
318- val up : Boolean ,
319- val broadcast : Boolean ,
320- val loopback : Boolean ,
321- val pointToPoint : Boolean ,
322- val multicast : Boolean ,
323- val addrs : List <AddrJson >,
329+ val name : String ,
330+ val index : Int ,
331+ val mtu : Int ,
332+ val up : Boolean ,
333+ val broadcast : Boolean ,
334+ val loopback : Boolean ,
335+ val pointToPoint : Boolean ,
336+ val multicast : Boolean ,
337+ val addrs : List <AddrJson >,
324338 )
339+
325340 override fun getInterfacesAsJson (): String {
326341 val interfaces = Collections .list(NetworkInterface .getNetworkInterfaces())
327342 val out = ArrayList <InterfaceJson >(interfaces.size)
328-
343+
329344 for (nif in interfaces) {
330345 try {
331346 val addrs = ArrayList <AddrJson >()
@@ -335,25 +350,24 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
335350 val host = addr.hostAddress ? : continue
336351 addrs.add(AddrJson (ip = host, prefixLen = ia.networkPrefixLength.toInt()))
337352 }
338-
353+
339354 out .add(
340- InterfaceJson (
341- name = nif.name,
342- index = nif.index,
343- mtu = nif.mtu,
344- up = nif.isUp,
345- broadcast = nif.supportsMulticast(),
346- loopback = nif.isLoopback,
347- pointToPoint = nif.isPointToPoint,
348- multicast = nif.supportsMulticast(),
349- addrs = addrs,
350- )
351- )
355+ InterfaceJson (
356+ name = nif.name,
357+ index = nif.index,
358+ mtu = nif.mtu,
359+ up = nif.isUp,
360+ broadcast = nif.supportsMulticast(),
361+ loopback = nif.isLoopback,
362+ pointToPoint = nif.isPointToPoint,
363+ multicast = nif.supportsMulticast(),
364+ addrs = addrs,
365+ ))
352366 } catch (_: Exception ) {
353367 continue
354368 }
355369 }
356-
370+
357371 // Avoid pretty printing to keep payload small.
358372 return Json { encodeDefaults = true }.encodeToString(out )
359373 }
@@ -394,48 +408,76 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
394408 app.notifyPolicyChanged()
395409 }
396410
397- override fun hardwareAttestationKeySupported (): Boolean {
398- return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ) {
399- packageManager.hasSystemFeature(PackageManager .FEATURE_STRONGBOX_KEYSTORE )
400- } else {
401- false
402- }
411+ override fun hardwareAttestationKeySupported (): Boolean {
412+ return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ) {
413+ packageManager.hasSystemFeature(PackageManager .FEATURE_STRONGBOX_KEYSTORE )
414+ } else {
415+ false
403416 }
417+ }
404418
405- private lateinit var keyStore: HardwareKeyStore ;
419+ private lateinit var keyStore: HardwareKeyStore
406420
407- private fun getKeyStore (): HardwareKeyStore {
408- if (hardwareAttestationKeySupported()) {
409- return HardwareKeyStore ()
410- } else {
411- throw UnsupportedOperationException ()
412- }
421+ private fun getKeyStore (): HardwareKeyStore {
422+ if (hardwareAttestationKeySupported()) {
423+ return HardwareKeyStore ()
424+ } else {
425+ throw UnsupportedOperationException ()
413426 }
427+ }
414428
415- override fun hardwareAttestationKeyCreate (): String {
416- return getKeyStore().createKey()
417- }
429+ override fun hardwareAttestationKeyCreate (): String {
430+ return getKeyStore().createKey()
431+ }
418432
419- @Throws(NoSuchKeyException ::class )
420- override fun hardwareAttestationKeyRelease (id : String ) {
421- return getKeyStore().releaseKey(id)
422- }
433+ @Throws(NoSuchKeyException ::class )
434+ override fun hardwareAttestationKeyRelease (id : String ) {
435+ return getKeyStore().releaseKey(id)
436+ }
423437
424- @Throws(NoSuchKeyException ::class )
425- override fun hardwareAttestationKeySign (id : String , data : ByteArray ): ByteArray {
426- return getKeyStore().sign(id, data)
427- }
438+ @Throws(NoSuchKeyException ::class )
439+ override fun hardwareAttestationKeySign (id : String , data : ByteArray ): ByteArray {
440+ return getKeyStore().sign(id, data)
441+ }
428442
429- @Throws(NoSuchKeyException ::class )
430- override fun hardwareAttestationKeyPublic (id : String ): ByteArray {
431- return getKeyStore().public(id)
432- }
443+ @Throws(NoSuchKeyException ::class )
444+ override fun hardwareAttestationKeyPublic (id : String ): ByteArray {
445+ return getKeyStore().public(id)
446+ }
447+
448+ @Throws(NoSuchKeyException ::class )
449+ override fun hardwareAttestationKeyLoad (id : String ) {
450+ return getKeyStore().load(id)
451+ }
433452
434- @Throws(NoSuchKeyException ::class )
435- override fun hardwareAttestationKeyLoad (id : String ) {
436- return getKeyStore().load(id)
453+ override fun bindSocketToNetwork (fd : Int ): Boolean {
454+ val net =
455+ NetworkChangeCallback .cachedDefaultNetwork
456+ ? : run {
457+ TSLog .d(TAG , " bindSocketToActiveNetwork: no cached default network; noop" )
458+ return false
459+ }
460+
461+ val iface = NetworkChangeCallback .cachedDefaultInterfaceName
462+
463+ TSLog .d(
464+ TAG ,
465+ " bindSocketToActiveNetwork: binding fd=$fd to net=$net iface=$iface " ,
466+ )
467+
468+ return try {
469+ android.os.ParcelFileDescriptor .fromFd(fd).use { pfd -> net.bindSocket(pfd.fileDescriptor) }
470+ true
471+ } catch (e: Exception ) {
472+ TSLog .w(
473+ TAG ,
474+ " bindSocketToActiveNetwork: bind failed fd=$fd net=$net iface=$iface : $e " ,
475+ )
476+ false
437477 }
478+ }
438479}
480+
439481/* *
440482 * UninitializedApp contains all of the methods of App that can be used without having to initialize
441483 * the Go backend. This is useful when you want to access functions on the App without creating side
@@ -499,12 +541,29 @@ open class UninitializedApp : Application() {
499541 return getSharedPreferences(UNENCRYPTED_PREFERENCES , MODE_PRIVATE )
500542 }
501543
544+ /* *
545+ * Starts IPNService as a foreground service without creating a VPN tunnel. This prevents Android
546+ * from freezing the process and restricting network access during interactive login while the
547+ * user completes auth in the browser.
548+ */
549+ fun startForegroundForLogin () {
550+ val intent =
551+ Intent (this , IPNService ::class .java).apply {
552+ action = IPNService .ACTION_START_FOREGROUND_ONLY
553+ }
554+ try {
555+ startForegroundService(intent)
556+ } catch (e: Exception ) {
557+ TSLog .e(TAG , " startForegroundForLogin hit exception: $e " )
558+ }
559+ }
560+
502561 fun startVPN () {
503562 val intent = Intent (this , IPNService ::class .java).apply { action = IPNService .ACTION_START_VPN }
504563 // FLAG_UPDATE_CURRENT ensures that if the intent is already pending, the existing intent will
505564 // be updated rather than creating multiple redundant instances.
506565 val pendingIntent =
507- PendingIntent .getService (
566+ PendingIntent .getForegroundService (
508567 this ,
509568 0 ,
510569 intent,
@@ -612,7 +671,7 @@ open class UninitializedApp : Application() {
612671 .setContentText(message)
613672 .setAutoCancel(! vpnRunning)
614673 .setOnlyAlertOnce(! vpnRunning)
615- .setOngoing(vpnRunning )
674+ .setOngoing(false )
616675 .setSilent(true )
617676 .setPriority(NotificationCompat .PRIORITY_DEFAULT )
618677 .setContentIntent(pendingIntent)
0 commit comments