Skip to content

Commit 63926a4

Browse files
authored
Merge branch 'tailscale:main' into main
2 parents 9f3161a + e5efc02 commit 63926a4

29 files changed

Lines changed: 692 additions & 360 deletions

.github/licenses.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The following open source dependencies are used to build the [Tailscale Android
1111
Client][]. See also the dependencies in the [Tailscale CLI][].
1212

1313
[Tailscale Android Client]: https://github.com/tailscale/tailscale-android
14+
[Tailscale CLI]: ./tailscale.md
1415

1516
## Go Packages
1617

.github/workflows/android.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ jobs:
3030
- name: Clean
3131
run: make clean
3232

33+
- name: Format check (ktfmt)
34+
run: make fmt-check
35+
36+
- name: Run Go tests
37+
run: make go-test
38+
3339
- name: Build APKs
3440
run: make tailscale-debug.apk
3541

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@ checkandroidsdk: ## Check that Android SDK is installed
315315
\tANDROID_SDK_ROOT=$(ANDROID_SDK_ROOT)\n\n\
316316
See README.md for instructions on how to install the prerequisites.\n"; exit 1)
317317

318+
.PHONY: go-test
319+
go-test: ## Run the Go tests (excludes packages requiring Android NDK)
320+
./tool/go test $$(./tool/go list ./... | grep -v '^github.com/tailscale/tailscale-android/libtailscale$$')
321+
318322
.PHONY: test
319323
test: gradle-dependencies ## Run the Android tests
320324
(cd android && ./gradlew test)

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 125 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ import java.io.IOException
4444
import java.lang.UnsupportedOperationException
4545
import java.net.NetworkInterface
4646
import java.security.GeneralSecurityException
47-
import java.util.Locale
4847
import java.util.Collections
48+
import java.util.Locale
4949
import kotlinx.coroutines.CoroutineScope
5050
import kotlinx.coroutines.Dispatchers
5151
import kotlinx.coroutines.SupervisorJob
@@ -54,11 +54,11 @@ import kotlinx.coroutines.flow.combine
5454
import kotlinx.coroutines.flow.distinctUntilChanged
5555
import kotlinx.coroutines.flow.first
5656
import kotlinx.coroutines.launch
57+
import kotlinx.serialization.Serializable
5758
import kotlinx.serialization.encodeToString
5859
import kotlinx.serialization.json.Json
59-
import kotlinx.serialization.encodeToString
60-
import kotlinx.serialization.Serializable
6160
import libtailscale.Libtailscale
61+
6262
class 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

Comments
 (0)