android: restructure app
Adds a product flavor for the new app Moves existing code out of main/ and into app/ - main/ contains shared code, app/ is for the existing app, and newapp/ is where all the new stuff will live Next: Add tasks (either in Gradle or in Makefile) to build the native library and use it for building the newapp Add in barebones main activity for newapp that launches Tailscale Updates tailscale/tailscale#10992 Signed-off-by: kari-ts <kari@tailscale.com>pull/180/head
parent
9492b01946
commit
a4d1a89580
@ -0,0 +1,68 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity android:name=".IPNActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.GioApp"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="multipart/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="multipart/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver android:name="IPNReceiver"
|
||||
android:exported="true"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="com.tailscale.ipn.CONNECT_VPN" />
|
||||
<action android:name="com.tailscale.ipn.DISCONNECT_VPN" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service android:name=".IPNService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".QuickToggleService"
|
||||
android:icon="@drawable/ic_tile"
|
||||
android:label="@string/tile_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity android:name="MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="multipart/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="multipart/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn
|
||||
|
||||
import android.util.Log
|
||||
import com.tailscale.ipn.ui.model.*
|
||||
import kotlinx.coroutines.*
|
||||
import android.app.Application
|
||||
import java.net.NetworkInterface
|
||||
import java.net.InterfaceAddress
|
||||
|
||||
class MyAppApplication : Application() {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("repository")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Perform application-wide initialization here.
|
||||
|
||||
// Example: Initialize a logging library
|
||||
initializeLogging()
|
||||
|
||||
// Example: Setup global error handling
|
||||
setupGlobalErrorHandler()
|
||||
}
|
||||
|
||||
private fun initializeLogging() {
|
||||
// Assuming a hypothetical logging setup
|
||||
Log.d("MyAppApplication", "Logging initialized.")
|
||||
}
|
||||
|
||||
private fun setupGlobalErrorHandler() {
|
||||
// Setup a custom uncaught exception handler for the application
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, e ->
|
||||
Log.e("MyAppApplication", "Uncaught exception in thread: ${thread.name}", e)
|
||||
// Here, you could report fatal errors to an analytics server, for example.
|
||||
}
|
||||
}
|
||||
|
||||
// Returns details of the interfaces in the system, encoded as a single string for ease
|
||||
// of JNI transfer over to the Go environment.
|
||||
//
|
||||
// Example:
|
||||
// rmnet_data0 10 2000 true false false false false | fe80::4059:dc16:7ed3:9c6e%rmnet_data0/64
|
||||
// dummy0 3 1500 true false false false false | fe80::1450:5cff:fe13:f891%dummy0/64
|
||||
// wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24
|
||||
// r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64
|
||||
// rmnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64
|
||||
// r_rmnet_data1 22 1500 true false false false false | fe80::b6cd:5cb0:8ae6:fe92%r_rmnet_data1/64
|
||||
// rmnet_data1 11 1500 true false false false false | fe80::51f2:ee00:edce:d68b%rmnet_data1/64
|
||||
// lo 1 65536 true false true false false | ::1/128 127.0.0.1/8
|
||||
// v4-rmnet_data2 68 1472 true true false true true | 192.0.0.4/32
|
||||
//
|
||||
// Where the fields are:
|
||||
// name ifindex mtu isUp hasBroadcast isLoopback isPointToPoint hasMulticast | ip1/N ip2/N ip3/N;
|
||||
fun getInterfacesAsString(): String {
|
||||
val interfaces: List<NetworkInterface> = try {
|
||||
NetworkInterface.getNetworkInterfaces().toList()
|
||||
} catch (e: Exception) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
for (nif in interfaces) {
|
||||
try {
|
||||
// Android doesn't have a supportsBroadcast() but the Go net.Interface wants
|
||||
// one, so we say the interface has broadcast if it has multicast.
|
||||
sb.append(String.format(java.util.Locale.ROOT, "%s %d %d %b %b %b %b %b |", nif.name,
|
||||
nif.index, nif.mtu, nif.isUp, nif.supportsMulticast(),
|
||||
nif.isLoopback, nif.isPointToPoint, nif.supportsMulticast()))
|
||||
|
||||
for (ia in nif.interfaceAddresses) {
|
||||
// InterfaceAddress == hostname + "/" + IP
|
||||
val parts = ia.toString().split("/", limit = 0)
|
||||
if (parts.size > 1) {
|
||||
sb.append(String.format(java.util.Locale.ROOT, "%s/%d ", parts[1], ia.networkPrefixLength))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// TODO: should log the exception not silently suppress it.
|
||||
continue
|
||||
}
|
||||
sb.append("\n")
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
Binary file not shown.
@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
import android.util.Log
|
||||
import com.tailscale.ipn.ui.model.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// A response from the echo endpoint.
|
||||
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
|
||||
|
||||
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
|
||||
|
||||
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
|
||||
|
||||
class LocalApiClient {
|
||||
constructor() {
|
||||
Log.d("LocalApiClient", "LocalApiClient created")
|
||||
}
|
||||
|
||||
// Perform a request to the local API in the go backend. This is
|
||||
// the primary JNI method for servicing a localAPI call. This
|
||||
// is GUARANTEED to call back into onResponse with the response
|
||||
// from the backend with a matching cookie.
|
||||
// @see cmd/localapiclient/localapishim.go
|
||||
//
|
||||
// request: The path to the localAPI endpoint.
|
||||
// method: The HTTP method to use.
|
||||
// body: The body of the request.
|
||||
// cookie: A unique identifier for this request. This is used map responses to
|
||||
// the corresponding request. Cookies must be unique for each request.
|
||||
external fun doRequest(request: String, method: String, body: String, cookie: String)
|
||||
|
||||
fun <T> executeRequest(request: LocalAPIRequest<T>) {
|
||||
Log.d("LocalApiClient", "Executing request:${request.method}:${request.path}")
|
||||
addRequest(request)
|
||||
// The jni handler will treat the empty string in the body as null.
|
||||
val body = request.body ?: ""
|
||||
doRequest(request.path, request.method, body, request.cookie)
|
||||
}
|
||||
|
||||
// This is called from the JNI layer to publish localAPIResponses. This should execute on the
|
||||
// same thread that called doRequest.
|
||||
fun onResponse(response: String, cookie: String) {
|
||||
val request = requests[cookie]
|
||||
if (request != null) {
|
||||
Log.d("LocalApiClient", "Reponse for request:${request.path} cookie:${request.cookie}")
|
||||
// The response handler will invoked internally by the request parser
|
||||
request.parser(response)
|
||||
removeRequest(cookie)
|
||||
} else {
|
||||
Log.e("LocalApiClient", "Received response for unknown request: ${cookie}")
|
||||
}
|
||||
}
|
||||
|
||||
// Tracks in-flight requests and their callback handlers by cookie. This should
|
||||
// always be manipulated via the addRequest and removeRequest methods.
|
||||
private var requests = HashMap<String, LocalAPIRequest<*>>()
|
||||
private var requestLock = Any()
|
||||
|
||||
fun addRequest(request: LocalAPIRequest<*>) {
|
||||
synchronized(requestLock) { requests[request.cookie] = request }
|
||||
}
|
||||
|
||||
fun removeRequest(cookie: String) {
|
||||
synchronized(requestLock) { requests.remove(cookie) }
|
||||
}
|
||||
|
||||
// localapi Invocations
|
||||
|
||||
fun getStatus(responseHandler: StatusResponseHandler) {
|
||||
val req = LocalAPIRequest.status(responseHandler)
|
||||
executeRequest<IpnState.Status>(req)
|
||||
}
|
||||
|
||||
fun getBugReportId(responseHandler: BugReportIdHandler) {
|
||||
val req = LocalAPIRequest.bugReportId(responseHandler)
|
||||
executeRequest<BugReportID>(req)
|
||||
}
|
||||
|
||||
fun getPrefs(responseHandler: PrefsHandler) {
|
||||
val req = LocalAPIRequest.prefs(responseHandler)
|
||||
executeRequest<Ipn.Prefs>(req)
|
||||
}
|
||||
|
||||
// (jonathan) TODO: A (likely) exhaustive list of localapi endpoints required for
|
||||
// a fully functioning client. This is a work in progress and will be updated
|
||||
// See: corp/xcode/Shared/LocalAPIClient.swift for the various verbs, parameters,
|
||||
// and body contents for each endpoint. Endpoints are defined in LocalAPIEndpoint
|
||||
//
|
||||
// fetchFileTargets
|
||||
// sendFiles
|
||||
// getWaitingFiles
|
||||
// recieveWaitingFile
|
||||
// inidicateFileRecieved
|
||||
// debug
|
||||
// debugLog
|
||||
// uploadClientMetrics
|
||||
// start
|
||||
// startLoginInteractive
|
||||
// logout
|
||||
// profiles
|
||||
// currentProfile
|
||||
// addProfile
|
||||
// switchProfile
|
||||
// deleteProfile
|
||||
// tailnetLocalStatus
|
||||
// signNode
|
||||
// verifyDeepling
|
||||
// ping
|
||||
// setTailFSFileServerAddress
|
||||
|
||||
// Run some tests to validate the APIs work before we have anything
|
||||
// that calls them. This runs after a short delay to avoid not-ready
|
||||
// errors
|
||||
// (jonathan) TODO: Do we need some kind of "onReady" callback?
|
||||
// (jonathan) TODO: Remove these we're further along
|
||||
|
||||
fun runAPITests() = runBlocking {
|
||||
delay(5000L)
|
||||
getStatus { result ->
|
||||
if (result.failed) {
|
||||
Log.e("LocalApiClient", "Error getting status: ${result.error}")
|
||||
} else {
|
||||
val status = result.success
|
||||
Log.d("LocalApiClient", "Got status: ${status}")
|
||||
}
|
||||
}
|
||||
|
||||
getBugReportId { result ->
|
||||
if (result.failed) {
|
||||
Log.e("LocalApiClient", "Error getting bug report id: ${result.error}")
|
||||
} else {
|
||||
val bugReportId = result.success
|
||||
Log.d("LocalApiClient", "Got bug report id: ${bugReportId}")
|
||||
}
|
||||
}
|
||||
|
||||
getPrefs { result ->
|
||||
if (result.failed) {
|
||||
Log.e("LocalApiClient", "Error getting prefs: ${result.error}")
|
||||
} else {
|
||||
val prefs = result.success
|
||||
Log.d("LocalApiClient", "Got prefs: ${prefs}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
import com.tailscale.ipn.ui.model.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
enum class LocalAPIEndpoint(val rawValue: String) {
|
||||
Debug("debug"),
|
||||
Debug_Log("debug-log"),
|
||||
BugReport("bugreport"),
|
||||
Prefs("prefs"),
|
||||
FileTargets("file-targets"),
|
||||
UploadMetrics("upload-client-metrics"),
|
||||
Start("start"),
|
||||
LoginInteractive("login-interactive"),
|
||||
ResetAuth("reset-auth"),
|
||||
Logout("logout"),
|
||||
Profiles("profiles"),
|
||||
ProfilesCurrent("profiles/current"),
|
||||
Status("status"),
|
||||
TKAStatus("tka/status"),
|
||||
TKASitng("tka/sign"),
|
||||
TKAVerifyDeeplink("tka/verify-deeplink"),
|
||||
Ping("ping"),
|
||||
Files("files"),
|
||||
FilePut("file-put"),
|
||||
TailFSServerAddress("tailfs/fileserver-address");
|
||||
|
||||
val prefix = "/localapi/v0/"
|
||||
|
||||
fun path(): String {
|
||||
return prefix + rawValue
|
||||
}
|
||||
}
|
||||
|
||||
// Potential local and upstream errors. Error handling in localapi in the go layer
|
||||
// is inconsistent but different clients already deal with that inconsistency so
|
||||
// 'fixing' it will likely break other things.
|
||||
//
|
||||
// For now, anything that isn't an { error: "message" } will be passed along
|
||||
// as UNPARSEABLE_RESPONSE. We can add additional error translation in the parseError
|
||||
// method as needed.
|
||||
//
|
||||
// (jonathan) TODO: Audit local API for all of the possible error results and clean
|
||||
// it up if possible.
|
||||
enum class APIErrorVals(val rawValue: String) {
|
||||
UNPARSEABLE_RESPONSE("Unparseable localAPI response"),
|
||||
NOT_READY("Not Ready");
|
||||
|
||||
fun toError(): Error {
|
||||
return Error(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalAPIRequest<T>(
|
||||
val path: String,
|
||||
val method: String,
|
||||
val body: String? = null,
|
||||
val responseHandler: (Result<T>) -> Unit,
|
||||
val parser: (String) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
val cookieLock = Any()
|
||||
var cookieCounter: Int = 0
|
||||
val decoder = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun getCookie(): String {
|
||||
synchronized(cookieLock) {
|
||||
cookieCounter += 1
|
||||
return cookieCounter.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun status(responseHandler: StatusResponseHandler): LocalAPIRequest<IpnState.Status> {
|
||||
val path = LocalAPIEndpoint.Status.path()
|
||||
return LocalAPIRequest<IpnState.Status>(path, "GET", null, responseHandler) { resp ->
|
||||
responseHandler(decode<IpnState.Status>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun bugReportId(responseHandler: BugReportIdHandler): LocalAPIRequest<BugReportID> {
|
||||
val path = LocalAPIEndpoint.BugReport.path()
|
||||
return LocalAPIRequest<BugReportID>(path, "POST", null, responseHandler) { resp ->
|
||||
responseHandler(parseString(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun prefs(responseHandler: PrefsHandler): LocalAPIRequest<Ipn.Prefs> {
|
||||
val path = LocalAPIEndpoint.Prefs.path()
|
||||
return LocalAPIRequest<Ipn.Prefs>(path, "GET", null, responseHandler) { resp ->
|
||||
responseHandler(decode<Ipn.Prefs>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the response was a generic error
|
||||
fun parseError(respData: String): Error {
|
||||
try {
|
||||
val err = Json.decodeFromString<Errors.GenericError>(respData)
|
||||
return Error(err.error)
|
||||
} catch (e: Exception) {
|
||||
return Error(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
||||
}
|
||||
}
|
||||
|
||||
// Handles responses that are raw strings. Returns an error result if the string
|
||||
// is empty
|
||||
fun parseString(respData: String): Result<String> {
|
||||
return if (respData.length > 0) Result(respData)
|
||||
else Result(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
||||
}
|
||||
|
||||
// Attempt to decode the response into the expected type. If that fails, then try
|
||||
// parsing as an error.
|
||||
inline fun <reified T> decode(respData: String): Result<T> {
|
||||
try {
|
||||
val message = decoder.decodeFromString<T>(respData)
|
||||
return Result(message)
|
||||
} catch (e: Exception) {
|
||||
return Result(parseError(respData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cookie: String = getCookie()
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
// Go-like result type with an optional value and an optional Error
|
||||
// This guarantees that only one of the two is non-null
|
||||
class Result<T> {
|
||||
val success: T?
|
||||
val error: Error?
|
||||
|
||||
constructor(success: T?, error: Error?) {
|
||||
if (success != null && error != null) {
|
||||
throw IllegalArgumentException("Result cannot have both a success and an error")
|
||||
}
|
||||
if (success == null && error == null) {
|
||||
throw IllegalArgumentException("Result must have either a success or an error")
|
||||
}
|
||||
|
||||
this.success = success
|
||||
this.error = error
|
||||
}
|
||||
|
||||
constructor(success: T) : this(success, null) {}
|
||||
constructor(error: Error) : this(null, error) {}
|
||||
|
||||
var successful: Boolean = false
|
||||
get() = success != null
|
||||
|
||||
var failed: Boolean = false
|
||||
get() = error != null
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.model
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
||||
class Dns {
|
||||
@Serializable data class HostEntry(val addr: Addr?, val hosts: List<String>?)
|
||||
|
||||
@Serializable
|
||||
data class OSConfig(
|
||||
val hosts: List<HostEntry>? = null,
|
||||
val nameservers: List<Addr>? = null,
|
||||
val searchDomains: List<String>? = null,
|
||||
val matchDomains: List<String>? = null,
|
||||
) {
|
||||
val isEmpty: Boolean
|
||||
get() = (hosts.isNullOrEmpty()) &&
|
||||
(nameservers.isNullOrEmpty()) &&
|
||||
(searchDomains.isNullOrEmpty()) &&
|
||||
(matchDomains.isNullOrEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
class DnsType {
|
||||
@Serializable
|
||||
data class Resolver(var Addr: String? = null, var BootstrapResolution: List<Addr>? = null)
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.model
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
||||
class Ipn {
|
||||
|
||||
// Represents the overall state of the Tailscale engine.
|
||||
enum class State(val value: Int) {
|
||||
NoState(0),
|
||||
InUseOtherUser(1),
|
||||
NeedsLogin(2),
|
||||
NeedsMachineAuth(3),
|
||||
Stopped(4),
|
||||
Starting(5),
|
||||
Running(6),
|
||||
}
|
||||
|
||||
// NotifyWatchOpt is a bitmask of options supplied to the notifier to specify which
|
||||
// what we want to see on the Noitfy bus
|
||||
enum class NotifyWatchOpt(val value: Int) {
|
||||
engineUpdates(0),
|
||||
initialState(1 shl 1),
|
||||
prefs(1 shl 2),
|
||||
netmap(1 shl 3),
|
||||
noPrivateKeys(1 shl 4),
|
||||
initialTailFSShares(1 shl 5)
|
||||
}
|
||||
|
||||
// A nofitication message recieved on the Notify bus. Fields will be populated based
|
||||
// on which NotifyWatchOpts were set when the Notifier was created.
|
||||
@Serializable
|
||||
data class Notify(
|
||||
val Version: String? = null,
|
||||
val ErrMessage: String? = null,
|
||||
val LoginFinished: Empty.Message? = null,
|
||||
val FilesWaiting: Empty.Message? = null,
|
||||
val State: State? = null,
|
||||
var Prefs: Prefs? = null,
|
||||
var NetMap: Netmap.NetworkMap? = null,
|
||||
var Engine: EngineStatus? = null,
|
||||
var BrowseToURL: String? = null,
|
||||
var BackendLogId: String? = null,
|
||||
var LocalTCPPort: Int? = null,
|
||||
var IncomingFiles: List<PartialFile>? = null,
|
||||
var ClientVersion: Tailcfg.ClientVersion? = null,
|
||||
var TailFSShares: Map<String, String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Prefs(
|
||||
var ControlURL: String = "",
|
||||
var RouteAll: Boolean = false,
|
||||
var AllowsSingleHosts: Boolean = false,
|
||||
var CorpDNS: Boolean = false,
|
||||
var WantRunning: Boolean = false,
|
||||
var LoggedOut: Boolean = false,
|
||||
var ShieldsUp: Boolean = false,
|
||||
var AdvertiseRoutes: List<String>? = null,
|
||||
var AdvertiseTags: List<String>? = null,
|
||||
var ExitNodeId: StableNodeID? = null,
|
||||
var ExitNodeAllowLanAccess: Boolean = false,
|
||||
var Config: Persist.Persist? = null,
|
||||
var ForceDaemon: Boolean = false,
|
||||
var HostName: String = "",
|
||||
var AutoUpdate: AutoUpdatePrefs? = AutoUpdatePrefs(true, true),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MaskedPrefs(
|
||||
var RouteAllSet: Boolean? = null,
|
||||
var CorpDNSSet: Boolean? = null,
|
||||
var ExitNodeIDSet: Boolean? = null,
|
||||
var ExitNodeAllowLANAccessSet: Boolean? = null,
|
||||
var WantRunningSet: Boolean? = null,
|
||||
var ShieldsUpSet: Boolean? = null,
|
||||
var AdvertiseRoutesSet: Boolean? = null,
|
||||
var ForceDaemonSet: Boolean? = null,
|
||||
var HostnameSet: Boolean? = null,
|
||||
) {
|
||||
var RouteAll: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
RouteAllSet = true
|
||||
}
|
||||
var CorpDNS: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
CorpDNSSet = true
|
||||
}
|
||||
var ExitNodeId: StableNodeID? = null
|
||||
set(value) {
|
||||
field = value
|
||||
ExitNodeIDSet = true
|
||||
}
|
||||
var ExitNodeAllowLanAccess: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
ExitNodeAllowLANAccessSet = true
|
||||
}
|
||||
var WantRunning: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
WantRunningSet = true
|
||||
}
|
||||
var ShieldsUp: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
ShieldsUpSet = true
|
||||
}
|
||||
var AdvertiseRoutes: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
AdvertiseRoutesSet = true
|
||||
}
|
||||
var ForceDaemon: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
ForceDaemonSet = true
|
||||
}
|
||||
var Hostname: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
HostnameSet = true
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AutoUpdatePrefs(
|
||||
var Check: Boolean? = null,
|
||||
var Apply: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EngineStatus(
|
||||
val RBytes: Long,
|
||||
val WBytes: Long,
|
||||
val NumLive: Int,
|
||||
val LivePeers: Map<String, IpnState.PeerStatusLite>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PartialFile(
|
||||
val Name: String,
|
||||
val Started: String,
|
||||
val DeclaredSize: Long,
|
||||
val Received: Long,
|
||||
val PartialPath: String? = null,
|
||||
var FinalPath: String? = null,
|
||||
val Done: Boolean? = null,
|
||||
)
|
||||
}
|
||||
|
||||
class Persist {
|
||||
@Serializable
|
||||
data class Persist(
|
||||
var PrivateMachineKey: String =
|
||||
"privkey:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
var PrivateNodeKey: String =
|
||||
"privkey:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
var OldPrivateNodeKey: String =
|
||||
"privkey:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
var Provider: String = "",
|
||||
)
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.model
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
||||
class IpnState {
|
||||
@Serializable
|
||||
data class PeerStatusLite(
|
||||
val RxBytes: Long,
|
||||
val TxBytes: Long,
|
||||
val LastHandshake: String,
|
||||
val NodeKey: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PeerStatus(
|
||||
val ID: StableNodeID,
|
||||
val HostName: String,
|
||||
val DNSName: String,
|
||||
val TailscaleIPs: List<Addr>? = null,
|
||||
val Tags: List<String>? = null,
|
||||
val PrimaryRoutes: List<String>? = null,
|
||||
val Addrs: List<String>? = null,
|
||||
val Online: Boolean,
|
||||
val ExitNode: Boolean,
|
||||
val ExitNodeOption: Boolean,
|
||||
val PeerAPIURL: List<String>? = null,
|
||||
val Capabilities: List<String>? = null,
|
||||
val SSH_HostKeys: List<String>? = null,
|
||||
val ShareeNode: Boolean? = null,
|
||||
val Expired: Boolean? = null,
|
||||
val Location: Tailcfg.Location? = null,
|
||||
) {
|
||||
fun computedName(status: Status): String {
|
||||
val name = DNSName
|
||||
val suffix = status.CurrentTailnet?.MagicDNSSuffix
|
||||
|
||||
suffix ?: return name
|
||||
|
||||
if (!(name.endsWith("." + suffix + "."))) {
|
||||
return name
|
||||
}
|
||||
|
||||
return name.dropLast(suffix.count() + 2)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ExitNodeStatus(
|
||||
val ID: StableNodeID,
|
||||
val Online: Boolean,
|
||||
val TailscaleIPs: List<Prefix>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TailnetStatus(
|
||||
val Name: String,
|
||||
val MagicDNSSuffix: String,
|
||||
val MagicDNSEnabled: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Status(
|
||||
val Version: String,
|
||||
val TUN: Boolean,
|
||||
val BackendState: String,
|
||||
val AuthURL: String,
|
||||
val TailscaleIPs: List<Addr>? = null,
|
||||
val Self: PeerStatus? = null,
|
||||
val ExitNodeStatus: ExitNodeStatus? = null,
|
||||
val Health: List<String>? = null,
|
||||
val CurrentTailnet: TailnetStatus? = null,
|
||||
val CertDomains: List<String>? = null,
|
||||
val Peer: Map<String, PeerStatus>? = null,
|
||||
val User: Map<String, Tailcfg.UserProfile>? = null,
|
||||
val ClientVersion: Tailcfg.ClientVersion? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NetworkLockStatus(
|
||||
var Enabled: Boolean,
|
||||
var PublicKey: String,
|
||||
var NodeKey: String,
|
||||
var NodeKeySigned: Boolean,
|
||||
var FilteredPeers: List<TKAFilteredPeer>? = null,
|
||||
var StateID: ULong? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TKAFilteredPeer(
|
||||
var Name: String,
|
||||
var TailscaleIPs: List<Addr>,
|
||||
var NodeKey: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PingResult(
|
||||
var IP: Addr,
|
||||
var Err: String,
|
||||
var LatencySeconds: Double,
|
||||
)
|
||||
}
|
||||
|
||||
class IpnLocal {
|
||||
@Serializable
|
||||
data class LoginProfile(
|
||||
var ID: String,
|
||||
val Name: String,
|
||||
val Key: String,
|
||||
val UserProfile: Tailcfg.UserProfile,
|
||||
val NetworkProfile: Tailcfg.NetworkProfile? = null,
|
||||
val LocalUserID: String,
|
||||
)
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.model
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
||||
class Netmap {
|
||||
@Serializable
|
||||
data class NetworkMap(
|
||||
var SelfNode: Tailcfg.Node,
|
||||
var NodeKey: KeyNodePublic,
|
||||
var Peers: List<Tailcfg.Node>? = null,
|
||||
var Expiry: Time,
|
||||
var Domain: String,
|
||||
var UserProfiles: Map<String, Tailcfg.UserProfile>,
|
||||
var TKAEnabled: Boolean,
|
||||
var DNS: Tailcfg.DNSConfig? = null
|
||||
) {
|
||||
// Keys are tailcfg.UserIDs thet get stringified
|
||||
// Helpers
|
||||
fun currentUserProfile(): Tailcfg.UserProfile? {
|
||||
return userProfile(User())
|
||||
}
|
||||
|
||||
fun User(): UserID {
|
||||
return SelfNode.User
|
||||
}
|
||||
|
||||
fun userProfile(id: Long): Tailcfg.UserProfile? {
|
||||
return UserProfiles[id.toString()]
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is NetworkMap) return false
|
||||
|
||||
return SelfNode == other.SelfNode &&
|
||||
NodeKey == other.NodeKey &&
|
||||
Peers == other.Peers &&
|
||||
Expiry == other.Expiry &&
|
||||
User() == other.User() &&
|
||||
Domain == other.Domain &&
|
||||
UserProfiles == other.UserProfiles &&
|
||||
TKAEnabled == other.TKAEnabled
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn.ui.model
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
||||
class Tailcfg {
|
||||
@Serializable
|
||||
data class ClientVersion(
|
||||
var RunningLatest: Boolean? = null,
|
||||
var LatestVersion: String? = null,
|
||||
var UrgentSecurityUpdate: Boolean? = null,
|
||||
var Notify: Boolean? = null,
|
||||
var NotifyURL: String? = null,
|
||||
var NotifyText: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserProfile(
|
||||
val ID: Long,
|
||||
val DisplayName: String,
|
||||
val LoginName: String,
|
||||
val ProfilePicURL: String? = null,
|
||||
) {
|
||||
fun isTaggedDevice(): Boolean {
|
||||
return LoginName == "tagged-devices"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Hostinfo(
|
||||
var IPNVersion: String? = null,
|
||||
var FrontendLogID: String? = null,
|
||||
var BackendLogID: String? = null,
|
||||
var OS: String? = null,
|
||||
var OSVersion: String? = null,
|
||||
var Env: String? = null,
|
||||
var Distro: String? = null,
|
||||
var DistroVersion: String? = null,
|
||||
var DistroCodeName: String? = null,
|
||||
var Desktop: Boolean? = null,
|
||||
var Package: String? = null,
|
||||
var DeviceModel: String? = null,
|
||||
var ShareeNode: Boolean? = null,
|
||||
var Hostname: String? = null,
|
||||
var ShieldsUp: Boolean? = null,
|
||||
var NoLogsNoSupport: Boolean? = null,
|
||||
var Machine: String? = null,
|
||||
var RoutableIPs: List<Prefix>? = null,
|
||||
var Services: List<Service>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Node(
|
||||
var ID: NodeID,
|
||||
var StableID: StableNodeID,
|
||||
var Name: String,
|
||||
var User: UserID,
|
||||
var Sharer: UserID? = null,
|
||||
var Key: KeyNodePublic,
|
||||
var KeyExpiry: String,
|
||||
var Machine: MachineKey,
|
||||
var Addresses: List<Prefix>? = null,
|
||||
var AllowedIPs: List<Prefix>? = null,
|
||||
var Endpoints: List<String>? = null,
|
||||
var Hostinfo: Hostinfo,
|
||||
var Created: Time,
|
||||
var LastSeen: Time? = null,
|
||||
var Online: Boolean? = null,
|
||||
var Capabilities: List<String>? = null,
|
||||
var ComputedName: String,
|
||||
var ComputedNameWithHost: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Service(var Proto: String, var Port: Int, var Description: String? = null)
|
||||
|
||||
@Serializable
|
||||
data class NetworkProfile(var MagicDNSName: String? = null, var DomainName: String? = null)
|
||||
|
||||
@Serializable
|
||||
data class Location(
|
||||
var Country: String? = null,
|
||||
var CountryCode: String? = null,
|
||||
var City: String? = null,
|
||||
var CityCode: String? = null,
|
||||
var Priority: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DNSConfig(
|
||||
var Resolvers: List<DnsType.Resolver>? = null,
|
||||
var Routes: Map<String, List<DnsType.Resolver>?>? = null,
|
||||
var FallbackResolvers: List<DnsType.Resolver>? = null,
|
||||
var Domains: List<String>? = null,
|
||||
var Nameservers: List<Addr>? = null
|
||||
)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
||||
package com.tailscale.ipn.ui.model
|
||||
import kotlinx.serialization.*
|
||||
|
||||
typealias Addr = String
|
||||
typealias Prefix = String
|
||||
typealias NodeID = Long
|
||||
typealias KeyNodePublic = String
|
||||
typealias MachineKey = String
|
||||
typealias UserID = Long
|
||||
typealias Time = String
|
||||
typealias StableNodeID = String
|
||||
typealias BugReportID = String
|
||||
|
||||
// Represents and empty message with a single 'property' field.
|
||||
class Empty {
|
||||
@Serializable
|
||||
data class Message(val property: String)
|
||||
}
|
||||
|
||||
// Parsable errors returned by localApiService
|
||||
class Errors {
|
||||
@Serializable
|
||||
data class GenericError(val error: String)
|
||||
}
|
||||
|
||||
// Returned on successful operations with no explicit response body
|
||||
class Success {
|
||||
@Serializable
|
||||
data class GenericSuccess(val message: String)
|
||||
}
|
@ -0,0 +1,665 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"github.com/tailscale/tailscale-android/cmd/jni"
|
||||
"github.com/tailscale/tailscale-android/cmd/localapiservice"
|
||||
"golang.org/x/sys/unix"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/must"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Wall
|
||||
|
||||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static jint jni_GetJavaVM(JNIEnv *env, JavaVM **jvm) {
|
||||
return (*env)->GetJavaVM(env, jvm);
|
||||
}
|
||||
|
||||
static jobject jni_NewGlobalRef(JNIEnv *env, jobject obj) {
|
||||
return (*env)->NewGlobalRef(env, obj);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type App struct {
|
||||
jvm *jni.JVM
|
||||
// appCtx is a global reference to the com.tailscale.ipn.TailscaleApp instance.
|
||||
appCtx jni.Object
|
||||
store *stateStore
|
||||
|
||||
logIDPublicAtomic atomic.Pointer[logid.PublicID]
|
||||
|
||||
localAPI *localapiservice.LocalAPIService
|
||||
backend *ipnlocal.LocalBackend
|
||||
}
|
||||
|
||||
type stateStore struct {
|
||||
jvm *jni.JVM
|
||||
// appCtx is the global Android app context.
|
||||
appCtx jni.Object
|
||||
|
||||
// Cached method ids on appCtx.
|
||||
encrypt jni.MethodID
|
||||
decrypt jni.MethodID
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
engine wgengine.Engine
|
||||
backend *ipnlocal.LocalBackend
|
||||
sys *tsd.System
|
||||
devices *multiTUN
|
||||
settings settingsFunc
|
||||
lastCfg *router.Config
|
||||
lastDNSCfg *dns.OSConfig
|
||||
netMon *netmon.Monitor
|
||||
|
||||
logIDPublic logid.PublicID
|
||||
logger *logtail.Logger
|
||||
|
||||
// avoidEmptyDNS controls whether to use fallback nameservers
|
||||
// when no nameservers are provided by Tailscale.
|
||||
avoidEmptyDNS bool
|
||||
|
||||
jvm *jni.JVM
|
||||
appCtx jni.Object
|
||||
}
|
||||
|
||||
type settingsFunc func(*router.Config, *dns.OSConfig) error
|
||||
|
||||
const defaultMTU = 1280 // minimalMTU from wgengine/userspace.go
|
||||
|
||||
// googleDNSServers are used on ChromeOS, where an empty VpnBuilder DNS setting results
|
||||
// in erasing the platform DNS servers. The developer docs say this is not supposed to happen,
|
||||
// but nonetheless it does.
|
||||
var googleDNSServers = []netip.Addr{
|
||||
netip.MustParseAddr("8.8.8.8"),
|
||||
netip.MustParseAddr("8.8.4.4"),
|
||||
netip.MustParseAddr("2001:4860:4860::8888"),
|
||||
netip.MustParseAddr("2001:4860:4860::8844"),
|
||||
}
|
||||
|
||||
func init() {
|
||||
a := App{}
|
||||
|
||||
err := localapiservice.ConfigureLocalApiJNIHandler(a.jvm, a.appCtx)
|
||||
if err != nil {
|
||||
fatalErr(err)
|
||||
}
|
||||
|
||||
interfaces.RegisterInterfaceGetter(a.getInterfaces)
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
if err := a.runBackend(ctx); err != nil {
|
||||
fatalErr(err)
|
||||
}
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
|
||||
func fatalErr(err error) {
|
||||
log.Printf("fatal error: %v", err)
|
||||
}
|
||||
|
||||
func (a *App) runBackend(ctx context.Context) error {
|
||||
appDir, err := app.DataDir()
|
||||
if err != nil {
|
||||
fatalErr(err)
|
||||
}
|
||||
paths.AppSharedDir.Store(appDir)
|
||||
|
||||
type configPair struct {
|
||||
rcfg *router.Config
|
||||
dcfg *dns.OSConfig
|
||||
}
|
||||
configs := make(chan configPair)
|
||||
configErrs := make(chan error)
|
||||
b, err := newBackend(appDir, a.jvm, a.appCtx, a.store, func(rcfg *router.Config, dcfg *dns.OSConfig) error {
|
||||
if rcfg == nil {
|
||||
return nil
|
||||
}
|
||||
configs <- configPair{rcfg, dcfg}
|
||||
return <-configErrs
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.logIDPublicAtomic.Store(&b.logIDPublic)
|
||||
a.backend = b.backend
|
||||
defer b.CloseTUNs()
|
||||
|
||||
h := localapi.NewHandler(b.backend, log.Printf, b.sys.NetMon.Get(), *a.logIDPublicAtomic.Load())
|
||||
h.PermitRead = true
|
||||
h.PermitWrite = true
|
||||
a.localAPI = localapiservice.New(h)
|
||||
|
||||
// Share the localAPI with the JNI shim
|
||||
localapiservice.SetLocalAPIService(a.localAPI)
|
||||
|
||||
startErr := make(chan error)
|
||||
// Start from a goroutine to avoid deadlock when Start
|
||||
// calls the callback.
|
||||
go func() {
|
||||
b.backend.Start(ipn.Options{})
|
||||
}()
|
||||
var (
|
||||
cfg configPair
|
||||
service jni.Object // of IPNService
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case err := <-startErr:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case s := <-configs:
|
||||
cfg = s
|
||||
if b == nil || service == 0 || cfg.rcfg == nil {
|
||||
configErrs <- nil
|
||||
break
|
||||
}
|
||||
configErrs <- b.updateTUN(service, cfg.rcfg, cfg.dcfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errVPNNotPrepared is used when VPNService.Builder.establish returns
|
||||
// null, either because the VPNService is not yet prepared or because
|
||||
// VPN status was revoked.
|
||||
var errVPNNotPrepared = errors.New("VPN service not prepared or was revoked")
|
||||
|
||||
// errMultipleUsers is used when we get a "INTERACT_ACROSS_USERS" error, which
|
||||
// happens due to a bug in Android. See:
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/2180
|
||||
var errMultipleUsers = errors.New("VPN cannot be created on this device due to an Android bug with multiple users")
|
||||
|
||||
func newBackend(dataDir string, jvm *jni.JVM, appCtx jni.Object, store *stateStore,
|
||||
settings settingsFunc) (*backend, error) {
|
||||
|
||||
sys := new(tsd.System)
|
||||
sys.Set(store)
|
||||
|
||||
logf := logger.RusagePrefixLog(log.Printf)
|
||||
b := &backend{
|
||||
jvm: jvm,
|
||||
devices: newTUNDevices(),
|
||||
settings: settings,
|
||||
appCtx: appCtx,
|
||||
}
|
||||
var logID logid.PrivateID
|
||||
logID.UnmarshalText([]byte("dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000"))
|
||||
storedLogID, err := store.read(logPrefKey)
|
||||
// In all failure cases we ignore any errors and continue with the dead value above.
|
||||
if err != nil || storedLogID == nil {
|
||||
// Read failed or there was no previous log id.
|
||||
newLogID, err := logid.NewPrivateID()
|
||||
if err == nil {
|
||||
logID = newLogID
|
||||
enc, err := newLogID.MarshalText()
|
||||
if err == nil {
|
||||
store.write(logPrefKey, enc)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logID.UnmarshalText([]byte(storedLogID))
|
||||
}
|
||||
|
||||
netMon, err := netmon.New(logf)
|
||||
if err != nil {
|
||||
log.Printf("netmon.New: %w", err)
|
||||
}
|
||||
b.netMon = netMon
|
||||
b.SetupLogs(dataDir, logID, logf)
|
||||
dialer := new(tsdial.Dialer)
|
||||
cb := &router.CallbackRouter{
|
||||
SetBoth: b.setCfg,
|
||||
SplitDNS: false,
|
||||
GetBaseConfigFunc: b.getDNSBaseConfig,
|
||||
}
|
||||
engine, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: b.devices,
|
||||
Router: cb,
|
||||
DNS: cb,
|
||||
Dialer: dialer,
|
||||
SetSubsystem: sys.Set,
|
||||
NetMon: b.netMon,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("runBackend: NewUserspaceEngine: %v", err)
|
||||
}
|
||||
sys.Set(engine)
|
||||
b.logIDPublic = logID.Public()
|
||||
ns, err := netstack.Create(logf, sys.Tun.Get(), engine, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("netstack.Create: %w", err)
|
||||
}
|
||||
sys.Set(ns)
|
||||
ns.ProcessLocalIPs = false // let Android kernel handle it; VpnBuilder sets this up
|
||||
ns.ProcessSubnets = true // for Android-being-an-exit-node support
|
||||
sys.NetstackRouter.Set(true)
|
||||
if w, ok := sys.Tun.GetOK(); ok {
|
||||
w.Start()
|
||||
}
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID.Public(), sys, 0)
|
||||
if err != nil {
|
||||
engine.Close()
|
||||
return nil, fmt.Errorf("runBackend: NewLocalBackend: %v", err)
|
||||
}
|
||||
if err := ns.Start(lb); err != nil {
|
||||
return nil, fmt.Errorf("startNetstack: %w", err)
|
||||
}
|
||||
if b.logger != nil {
|
||||
lb.SetLogFlusher(b.logger.StartFlush)
|
||||
}
|
||||
b.engine = engine
|
||||
b.backend = lb
|
||||
b.sys = sys
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// SetupLogs sets up remote logging.
|
||||
func (b *backend) SetupLogs(logDir string, logID logid.PrivateID, logf logger.Logf) {
|
||||
if b.netMon == nil {
|
||||
panic("netMon must be created prior to SetupLogs")
|
||||
}
|
||||
transport := logpolicy.NewLogtailTransport(logtail.DefaultHost, b.netMon, log.Printf)
|
||||
|
||||
logcfg := logtail.Config{
|
||||
Collection: logtail.CollectionNode,
|
||||
PrivateID: logID,
|
||||
Stderr: log.Writer(),
|
||||
MetricsDelta: clientmetric.EncodeLogTailMetricsDelta,
|
||||
IncludeProcID: true,
|
||||
IncludeProcSequence: true,
|
||||
NewZstdEncoder: func() logtail.Encoder {
|
||||
return must.Get(smallzstd.NewEncoder(nil))
|
||||
},
|
||||
HTTPC: &http.Client{Transport: transport},
|
||||
}
|
||||
logcfg.FlushDelayFn = func() time.Duration { return 2 * time.Minute }
|
||||
|
||||
filchOpts := filch.Options{
|
||||
ReplaceStderr: true,
|
||||
}
|
||||
|
||||
var filchErr error
|
||||
if logDir != "" {
|
||||
logPath := filepath.Join(logDir, "ipn.log.")
|
||||
logcfg.Buffer, filchErr = filch.New(logPath, filchOpts)
|
||||
}
|
||||
|
||||
b.logger = logtail.NewLogger(logcfg, logf)
|
||||
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(b.logger)
|
||||
|
||||
log.Printf("goSetupLogs: success")
|
||||
|
||||
if logDir == "" {
|
||||
log.Printf("SetupLogs: no logDir, storing logs in memory")
|
||||
}
|
||||
if filchErr != nil {
|
||||
log.Printf("SetupLogs: filch setup failed: %v", filchErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) NetworkChanged() {
|
||||
if b.sys != nil {
|
||||
if nm, ok := b.sys.NetMon.GetOK(); ok {
|
||||
nm.InjectEvent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) setCfg(rcfg *router.Config, dcfg *dns.OSConfig) error {
|
||||
return b.settings(rcfg, dcfg)
|
||||
}
|
||||
|
||||
func (b *backend) updateTUN(service jni.Object, rcfg *router.Config, dcfg *dns.OSConfig) error {
|
||||
if reflect.DeepEqual(rcfg, b.lastCfg) && reflect.DeepEqual(dcfg, b.lastDNSCfg) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close previous tunnel(s).
|
||||
// This is necessary for ChromeOS, native Android devices
|
||||
// seem to handle seamless handover between tunnels correctly.
|
||||
//
|
||||
// TODO(eliasnaur): If seamless handover becomes a desirable feature, skip
|
||||
// the closing on ChromeOS.
|
||||
b.CloseTUNs()
|
||||
|
||||
if len(rcfg.LocalAddrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
err := jni.Do(b.jvm, func(env *jni.Env) error {
|
||||
cls := jni.GetObjectClass(env, service)
|
||||
// Construct a VPNService.Builder. IPNService.newBuilder calls
|
||||
// setConfigureIntent, and allowFamily for both IPv4 and IPv6.
|
||||
m := jni.GetMethodID(env, cls, "newBuilder", "()Landroid/net/VpnService$Builder;")
|
||||
builder, err := jni.CallObjectMethod(env, service, m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IPNService.newBuilder: %v", err)
|
||||
}
|
||||
bcls := jni.GetObjectClass(env, builder)
|
||||
|
||||
// builder.setMtu.
|
||||
setMtu := jni.GetMethodID(env, bcls, "setMtu", "(I)Landroid/net/VpnService$Builder;")
|
||||
const mtu = defaultMTU
|
||||
if _, err := jni.CallObjectMethod(env, builder, setMtu, jni.Value(mtu)); err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.setMtu: %v", err)
|
||||
}
|
||||
|
||||
// builder.addDnsServer
|
||||
addDnsServer := jni.GetMethodID(env, bcls, "addDnsServer", "(Ljava/lang/String;)Landroid/net/VpnService$Builder;")
|
||||
// builder.addSearchDomain.
|
||||
addSearchDomain := jni.GetMethodID(env, bcls, "addSearchDomain", "(Ljava/lang/String;)Landroid/net/VpnService$Builder;")
|
||||
if dcfg != nil {
|
||||
nameservers := dcfg.Nameservers
|
||||
if b.avoidEmptyDNS && len(nameservers) == 0 {
|
||||
nameservers = googleDNSServers
|
||||
}
|
||||
for _, dns := range nameservers {
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addDnsServer,
|
||||
jni.Value(jni.JavaString(env, dns.String())),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addDnsServer(%v): %v", dns, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dom := range dcfg.SearchDomains {
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addSearchDomain,
|
||||
jni.Value(jni.JavaString(env, dom.WithoutTrailingDot())),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addSearchDomain(%v): %v", dom, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// builder.addRoute.
|
||||
addRoute := jni.GetMethodID(env, bcls, "addRoute", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
|
||||
for _, route := range rcfg.Routes {
|
||||
// Normalize route address; Builder.addRoute does not accept non-zero masked bits.
|
||||
route = route.Masked()
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addRoute,
|
||||
jni.Value(jni.JavaString(env, route.Addr().String())),
|
||||
jni.Value(route.Bits()),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addRoute(%v): %v", route, err)
|
||||
}
|
||||
}
|
||||
|
||||
// builder.addAddress.
|
||||
addAddress := jni.GetMethodID(env, bcls, "addAddress", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
|
||||
for _, addr := range rcfg.LocalAddrs {
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addAddress,
|
||||
jni.Value(jni.JavaString(env, addr.Addr().String())),
|
||||
jni.Value(addr.Bits()),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addAddress(%v): %v", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// builder.establish.
|
||||
establish := jni.GetMethodID(env, bcls, "establish", "()Landroid/os/ParcelFileDescriptor;")
|
||||
parcelFD, err := jni.CallObjectMethod(env, builder, establish)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "INTERACT_ACROSS_USERS") {
|
||||
return errMultipleUsers
|
||||
}
|
||||
return fmt.Errorf("VpnService.Builder.establish: %v", err)
|
||||
}
|
||||
if parcelFD == 0 {
|
||||
return errVPNNotPrepared
|
||||
}
|
||||
|
||||
// detachFd.
|
||||
parcelCls := jni.GetObjectClass(env, parcelFD)
|
||||
detachFd := jni.GetMethodID(env, parcelCls, "detachFd", "()I")
|
||||
tunFD, err := jni.CallIntMethod(env, parcelFD, detachFd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detachFd: %v", err)
|
||||
}
|
||||
|
||||
// Create TUN device.
|
||||
tunDev, _, err := tun.CreateUnmonitoredTUNFromFD(int(tunFD))
|
||||
if err != nil {
|
||||
unix.Close(int(tunFD))
|
||||
return err
|
||||
}
|
||||
|
||||
b.devices.add(tunDev)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
b.lastCfg = nil
|
||||
b.CloseTUNs()
|
||||
return err
|
||||
}
|
||||
b.lastCfg = rcfg
|
||||
b.lastDNSCfg = dcfg
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseVPN closes any active TUN devices.
|
||||
func (b *backend) CloseTUNs() {
|
||||
b.lastCfg = nil
|
||||
b.devices.Shutdown()
|
||||
}
|
||||
|
||||
// Report interfaces in the device in net.Interface format.
|
||||
func (a *App) getInterfaces() ([]interfaces.Interface, error) {
|
||||
var ifaceString string
|
||||
err := jni.Do(a.jvm, func(env *jni.Env) error {
|
||||
cls := jni.GetObjectClass(env, a.appCtx)
|
||||
m := jni.GetMethodID(env, cls, "getInterfacesAsString", "()Ljava/lang/String;")
|
||||
n, err := jni.CallObjectMethod(env, a.appCtx, m)
|
||||
ifaceString = jni.GoString(env, jni.String(n))
|
||||
return err
|
||||
|
||||
})
|
||||
var ifaces []interfaces.Interface
|
||||
if err != nil {
|
||||
return ifaces, err
|
||||
}
|
||||
|
||||
for _, iface := range strings.Split(ifaceString, "\n") {
|
||||
// Example of the strings we're processing:
|
||||
// wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24
|
||||
// r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64
|
||||
// mnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64
|
||||
|
||||
if strings.TrimSpace(iface) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Split(iface, "|")
|
||||
if len(fields) != 2 {
|
||||
log.Printf("getInterfaces: unable to split %q", iface)
|
||||
continue
|
||||
}
|
||||
|
||||
var name string
|
||||
var index, mtu int
|
||||
var up, broadcast, loopback, pointToPoint, multicast bool
|
||||
_, err := fmt.Sscanf(fields[0], "%s %d %d %t %t %t %t %t",
|
||||
&name, &index, &mtu, &up, &broadcast, &loopback, &pointToPoint, &multicast)
|
||||
if err != nil {
|
||||
log.Printf("getInterfaces: unable to parse %q: %v", iface, err)
|
||||
continue
|
||||
}
|
||||
|
||||
newIf := interfaces.Interface{
|
||||
Interface: &net.Interface{
|
||||
Name: name,
|
||||
Index: index,
|
||||
MTU: mtu,
|
||||
},
|
||||
AltAddrs: []net.Addr{}, // non-nil to avoid Go using netlink
|
||||
}
|
||||
if up {
|
||||
newIf.Flags |= net.FlagUp
|
||||
}
|
||||
if broadcast {
|
||||
newIf.Flags |= net.FlagBroadcast
|
||||
}
|
||||
if loopback {
|
||||
newIf.Flags |= net.FlagLoopback
|
||||
}
|
||||
if pointToPoint {
|
||||
newIf.Flags |= net.FlagPointToPoint
|
||||
}
|
||||
if multicast {
|
||||
newIf.Flags |= net.FlagMulticast
|
||||
}
|
||||
|
||||
addrs := strings.Trim(fields[1], " \n")
|
||||
for _, addr := range strings.Split(addrs, " ") {
|
||||
ip, err := netaddr.ParseIPPrefix(addr)
|
||||
if err == nil {
|
||||
newIf.AltAddrs = append(newIf.AltAddrs, ip.IPNet())
|
||||
}
|
||||
}
|
||||
|
||||
ifaces = append(ifaces, newIf)
|
||||
}
|
||||
|
||||
return ifaces, nil
|
||||
}
|
||||
|
||||
func (b *backend) getPlatformDNSConfig() string {
|
||||
var baseConfig string
|
||||
err := jni.Do(b.jvm, func(env *jni.Env) error {
|
||||
cls := jni.GetObjectClass(env, b.appCtx)
|
||||
m := jni.GetMethodID(env, cls, "getDnsConfigObj", "()Lcom/tailscale/ipn/DnsConfig;")
|
||||
dns, err := jni.CallObjectMethod(env, b.appCtx, m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDnsConfigObj: %v", err)
|
||||
}
|
||||
dnsCls := jni.GetObjectClass(env, dns)
|
||||
m = jni.GetMethodID(env, dnsCls, "getDnsConfigAsString", "()Ljava/lang/String;")
|
||||
n, err := jni.CallObjectMethod(env, dns, m)
|
||||
baseConfig = jni.GoString(env, jni.String(n))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("getPlatformDNSConfig JNI: %v", err)
|
||||
return ""
|
||||
}
|
||||
return baseConfig
|
||||
}
|
||||
|
||||
func (b *backend) getDNSBaseConfig() (ret dns.OSConfig, _ error) {
|
||||
defer func() {
|
||||
// If we couldn't find any base nameservers, ultimately fall back to
|
||||
// Google's. Normally Tailscale doesn't ever pick a default nameserver
|
||||
// for users but in this case Android's APIs for reading the underlying
|
||||
// DNS config are lacking, and almost all Android phones use Google
|
||||
// services anyway, so it's a reasonable default: it's an ecosystem the
|
||||
// user has selected by having an Android device.
|
||||
if len(ret.Nameservers) == 0 && googleSignInEnabled() {
|
||||
log.Printf("getDNSBaseConfig: none found; falling back to Google public DNS")
|
||||
ret.Nameservers = append(ret.Nameservers, googleDNSServers...)
|
||||
}
|
||||
}()
|
||||
baseConfig := b.getPlatformDNSConfig()
|
||||
lines := strings.Split(baseConfig, "\n")
|
||||
if len(lines) == 0 {
|
||||
return dns.OSConfig{}, nil
|
||||
}
|
||||
|
||||
config := dns.OSConfig{}
|
||||
addrs := strings.Trim(lines[0], " \n")
|
||||
for _, addr := range strings.Split(addrs, " ") {
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err == nil {
|
||||
config.Nameservers = append(config.Nameservers, ip)
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) > 1 {
|
||||
for _, s := range strings.Split(strings.Trim(lines[1], " \n"), " ") {
|
||||
domain, err := dnsname.ToFQDN(s)
|
||||
if err != nil {
|
||||
log.Printf("getDNSBaseConfig: unable to parse %q: %v", s, err)
|
||||
continue
|
||||
}
|
||||
config.SearchDomains = append(config.SearchDomains, domain)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// TODO
|
||||
func googleSignInEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_TailscaleApp_initRepository
|
||||
func Java_com_tailscale_ipn_TailscaleApp_initRepository(env *C.JNIEnv, class C.jclass, jdataDir C.jbyteArray, context C.jobject) {
|
||||
initJVM(env, class, context)
|
||||
}
|
||||
|
||||
func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) {
|
||||
if res := C.jni_GetJavaVM(env, &App.jvm); res != 0 {
|
||||
panic("GetJavaVM failed")
|
||||
}
|
||||
App.appCtx = C.jni_NewGlobalRef(env, ctx)
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
||||
|
||||
/* package github.com/tailscale/tailscale-android/cmd/tailscale */
|
||||
|
||||
|
||||
#line 1 "cgo-builtin-export-prolog"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
||||
#define GO_CGO_EXPORT_PROLOGUE_H
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
/* Start of preamble from import "C" comments. */
|
||||
|
||||
|
||||
#line 15 "callbacks.go"
|
||||
#include <jni.h>
|
||||
|
||||
#line 1 "cgo-generated-wrapper"
|
||||
|
||||
|
||||
/* End of preamble from import "C" comments. */
|
||||
|
||||
|
||||
/* Start of boilerplate cgo prologue. */
|
||||
#line 1 "cgo-gcc-export-header-prolog"
|
||||
|
||||
#ifndef GO_CGO_PROLOGUE_H
|
||||
#define GO_CGO_PROLOGUE_H
|
||||
|
||||
typedef signed char GoInt8;
|
||||
typedef unsigned char GoUint8;
|
||||
typedef short GoInt16;
|
||||
typedef unsigned short GoUint16;
|
||||
typedef int GoInt32;
|
||||
typedef unsigned int GoUint32;
|
||||
typedef long long GoInt64;
|
||||
typedef unsigned long long GoUint64;
|
||||
typedef GoInt64 GoInt;
|
||||
typedef GoUint64 GoUint;
|
||||
typedef size_t GoUintptr;
|
||||
typedef float GoFloat32;
|
||||
typedef double GoFloat64;
|
||||
#ifdef _MSC_VER
|
||||
#include <complex.h>
|
||||
typedef _Fcomplex GoComplex64;
|
||||
typedef _Dcomplex GoComplex128;
|
||||
#else
|
||||
typedef float _Complex GoComplex64;
|
||||
typedef double _Complex GoComplex128;
|
||||
#endif
|
||||
|
||||
/*
|
||||
static assertion to make sure the file is being used on architecture
|
||||
at least with matching size of GoInt.
|
||||
*/
|
||||
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef _GoString_ GoString;
|
||||
#endif
|
||||
typedef void *GoMap;
|
||||
typedef void *GoChan;
|
||||
typedef struct { void *t; void *v; } GoInterface;
|
||||
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||
|
||||
#endif
|
||||
|
||||
/* End of boilerplate cgo prologue. */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern void Java_com_tailscale_ipn_App_onVPNPrepared(JNIEnv* env, jclass class);
|
||||
extern void Java_com_tailscale_ipn_App_onWriteStorageGranted(JNIEnv* env, jclass class);
|
||||
extern void Java_com_tailscale_ipn_IPNService_connect(JNIEnv* env, jobject this);
|
||||
extern void Java_com_tailscale_ipn_IPNService_directConnect(JNIEnv* env, jobject this);
|
||||
extern void Java_com_tailscale_ipn_IPNService_disconnect(JNIEnv* env, jobject this);
|
||||
extern void Java_com_tailscale_ipn_StartVPNWorker_connect(JNIEnv* env, jobject this);
|
||||
extern void Java_com_tailscale_ipn_StopVPNWorker_disconnect(JNIEnv* env, jobject this);
|
||||
extern void Java_com_tailscale_ipn_Peer_onActivityResult0(JNIEnv* env, jclass cls, jobject act, jint reqCode, jint resCode);
|
||||
extern void Java_com_tailscale_ipn_App_onShareIntent(JNIEnv* env, jclass cls, jint nfiles, jintArray jtypes, jobjectArray jmimes, jobjectArray jitems, jobjectArray jnames, jlongArray jsizes);
|
||||
extern void Java_com_tailscale_ipn_App_onDnsConfigChanged(JNIEnv* env, jclass cls);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
Loading…
Reference in New Issue