diff --git a/Makefile b/Makefile index 2e5337e..472d70e 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,7 @@ install: tailscale-debug.apk ## Install the debug APK on a connected device adb install -r $< run: install ## Run the debug APK on a connected device - adb shell am start -n com.tailscale.ipn/com.tailscale.ipn.IPNActivity + adb shell am start -n com.tailscale.ipn/com.tailscale.ipn.MainActivity dockershell: ## Run a shell in the Docker build container docker build -t tailscale-android . @@ -191,5 +191,5 @@ help: ## Show this help @grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}' @echo "" -.PHONY: all clean install android_legacy/lib $(DEBUG_APK) $(RELEASE_AAB) release bump_version dockershell lib tailscale-debug help +.PHONY: all clean install android/lib $(DEBUG_APK) $(RELEASE_AAB) release bump_version dockershell lib tailscale-debug help .DEFAULT_GOAL := help diff --git a/android_legacy/build.gradle b/android_legacy/build.gradle deleted file mode 100644 index b52185c..0000000 --- a/android_legacy/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ - -buildscript { - ext.kotlin_version = "1.9.22" - - repositories { - google() - mavenCentral() - } - dependencies { - classpath "com.android.tools.build:gradle:8.1.0" - } -} - -repositories { - google() - mavenCentral() - flatDir { - dirs 'libs' - } -} - -apply plugin: 'com.android.application' - -android { - ndkVersion "23.1.7779620" - compileSdk 33 - defaultConfig { - minSdkVersion 22 - targetSdkVersion 33 - versionCode 201 - versionName "1.61.105-t7429e8912-g12210d3f26b" - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - flavorDimensions "version" - productFlavors { - fdroid { - // The fdroid flavor contains only free dependencies and is suitable - // for the F-Droid app store. - } - play { - // The play flavor contains all features and is for the Play Store. - } - } - namespace 'com.tailscale.ipn' -} - -dependencies { - implementation "androidx.core:core:1.9.0" - implementation "androidx.browser:browser:1.5.0" - implementation "androidx.security:security-crypto:1.1.0-alpha06" - implementation "androidx.work:work-runtime:2.8.1" - implementation ':ipn@aar' - testImplementation "junit:junit:4.12" - - // Non-free dependencies. - playImplementation 'com.google.android.gms:play-services-auth:20.7.0' -} diff --git a/android_legacy/gradle.properties b/android_legacy/gradle.properties deleted file mode 100644 index eb5eece..0000000 --- a/android_legacy/gradle.properties +++ /dev/null @@ -1,5 +0,0 @@ -android.defaults.buildfeatures.buildconfig=true -android.nonFinalResIds=false -android.nonTransitiveRClass=false -android.useAndroidX=true -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m diff --git a/android_legacy/gradle/wrapper/gradle-wrapper.jar b/android_legacy/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 490fda8..0000000 Binary files a/android_legacy/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android_legacy/gradle/wrapper/gradle-wrapper.properties b/android_legacy/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index dd1d9c0..0000000 --- a/android_legacy/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android_legacy/gradlew b/android_legacy/gradlew deleted file mode 100755 index 3368417..0000000 --- a/android_legacy/gradlew +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx80m" "-Xms80m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/android_legacy/gradlew.bat b/android_legacy/gradlew.bat deleted file mode 100644 index 22fc358..0000000 --- a/android_legacy/gradlew.bat +++ /dev/null @@ -1,103 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx80m" "-Xms80m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android_legacy/src/main/AndroidManifest.xml b/android_legacy/src/main/AndroidManifest.xml deleted file mode 100644 index 120d404..0000000 --- a/android_legacy/src/main/AndroidManifest.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android_legacy/src/main/ic_launcher-playstore.png b/android_legacy/src/main/ic_launcher-playstore.png deleted file mode 100644 index bae6d31..0000000 Binary files a/android_legacy/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/android_legacy/src/main/java/com/tailscale/ipn/App.java b/android_legacy/src/main/java/com/tailscale/ipn/App.java deleted file mode 100644 index e303227..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/App.java +++ /dev/null @@ -1,427 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.app.Application; -import android.app.Activity; -import android.app.DownloadManager; -import android.app.Fragment; -import android.app.FragmentTransaction; -import android.app.NotificationChannel; -import android.app.PendingIntent; -import android.app.UiModeManager; -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.Signature; -import android.content.res.Configuration; -import android.provider.MediaStore; -import android.provider.Settings; -import android.net.ConnectivityManager; -import android.net.LinkProperties; -import android.net.Network; -import android.net.NetworkInfo; -import android.net.NetworkRequest; -import android.net.Uri; -import android.net.VpnService; -import android.view.View; -import android.os.Build; -import android.os.Environment; -import android.os.Handler; -import android.os.Looper; - -import android.Manifest; -import android.webkit.MimeTypeMap; - -import java.io.IOException; -import java.io.File; -import java.io.FileOutputStream; - -import java.lang.StringBuilder; - -import java.net.InetAddress; -import java.net.InterfaceAddress; -import java.net.NetworkInterface; - -import java.security.GeneralSecurityException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import androidx.core.content.ContextCompat; - -import androidx.security.crypto.EncryptedSharedPreferences; -import androidx.security.crypto.MasterKey; - -import androidx.browser.customtabs.CustomTabsIntent; - -import org.gioui.Gio; - -public class App extends Application { - private static final String PEER_TAG = "peer"; - - static final String STATUS_CHANNEL_ID = "tailscale-status"; - static final int STATUS_NOTIFICATION_ID = 1; - - static final String NOTIFY_CHANNEL_ID = "tailscale-notify"; - static final int NOTIFY_NOTIFICATION_ID = 2; - - private static final String FILE_CHANNEL_ID = "tailscale-files"; - private static final int FILE_NOTIFICATION_ID = 3; - - private static final Handler mainHandler = new Handler(Looper.getMainLooper()); - - private ConnectivityManager connectivityManager; - public DnsConfig dns = new DnsConfig(); - public DnsConfig getDnsConfigObj() { return this.dns; } - - @Override public void onCreate() { - super.onCreate(); - // Load and initialize the Go library. - Gio.init(this); - - this.connectivityManager = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE); - setAndRegisterNetworkCallbacks(); - - createNotificationChannel(NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT); - createNotificationChannel(STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW); - createNotificationChannel(FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT); - } - - // requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is possible that - // this might return an unusuable network, eg a captive portal. - private void setAndRegisterNetworkCallbacks() { - connectivityManager.requestNetwork(dns.getDNSConfigNetworkRequest(), new ConnectivityManager.NetworkCallback(){ - @Override - public void onAvailable(Network network){ - super.onAvailable(network); - StringBuilder sb = new StringBuilder(""); - LinkProperties linkProperties = connectivityManager.getLinkProperties(network); - List dnsList = linkProperties.getDnsServers(); - for (InetAddress ip : dnsList) { - sb.append(ip.getHostAddress()).append(" "); - } - String searchDomains = linkProperties.getDomains(); - if (searchDomains != null) { - sb.append("\n"); - sb.append(searchDomains); - } - - dns.updateDNSFromNetwork(sb.toString()); - onDnsConfigChanged(); - } - - @Override - public void onLost(Network network) { - super.onLost(network); - onDnsConfigChanged(); - } - }); - } - - public void startVPN() { - Intent intent = new Intent(this, IPNService.class); - intent.setAction(IPNService.ACTION_REQUEST_VPN); - startService(intent); - } - - public void stopVPN() { - Intent intent = new Intent(this, IPNService.class); - intent.setAction(IPNService.ACTION_STOP_VPN); - startService(intent); - } - - // encryptToPref a byte array of data using the Jetpack Security - // library and writes it to a global encrypted preference store. - public void encryptToPref(String prefKey, String plaintext) throws IOException, GeneralSecurityException { - getEncryptedPrefs().edit().putString(prefKey, plaintext).commit(); - } - - // decryptFromPref decrypts a encrypted preference using the Jetpack Security - // library and returns the plaintext. - public String decryptFromPref(String prefKey) throws IOException, GeneralSecurityException { - return getEncryptedPrefs().getString(prefKey, null); - } - - private SharedPreferences getEncryptedPrefs() throws IOException, GeneralSecurityException { - MasterKey key = new MasterKey.Builder(this) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build(); - - return EncryptedSharedPreferences.create( - this, - "secret_shared_prefs", - key, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ); - } - - public boolean autoConnect = false; - public boolean vpnReady = false; - - void setTileReady(boolean ready) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return; - } - QuickToggleService.setReady(this, ready); - android.util.Log.d("App", "Set Tile Ready: " + ready + " " + autoConnect); - - vpnReady = ready; - if (ready && autoConnect) { - startVPN(); - } - } - - void setTileStatus(boolean status) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return; - } - QuickToggleService.setStatus(this, status); - } - - String getHostname() { - String userConfiguredDeviceName = getUserConfiguredDeviceName(); - if (!isEmpty(userConfiguredDeviceName)) return userConfiguredDeviceName; - - return getModelName(); - } - - String getModelName() { - String manu = Build.MANUFACTURER; - String model = Build.MODEL; - // Strip manufacturer from model. - int idx = model.toLowerCase().indexOf(manu.toLowerCase()); - if (idx != -1) { - model = model.substring(idx + manu.length()); - model = model.trim(); - } - return manu + " " + model; - } - - String getOSVersion() { - return Build.VERSION.RELEASE; - } - - // get user defined nickname from Settings - // returns null if not available - private String getUserConfiguredDeviceName() { - String nameFromSystemDevice = Settings.Secure.getString(getContentResolver(), "device_name"); - if (!isEmpty(nameFromSystemDevice)) return nameFromSystemDevice; - return null; - } - - private static boolean isEmpty(String str) { - return str == null || str.length() == 0; - } - - // attachPeer adds a Peer fragment for tracking the Activity - // lifecycle. - void attachPeer(Activity act) { - act.runOnUiThread(new Runnable() { - @Override public void run() { - FragmentTransaction ft = act.getFragmentManager().beginTransaction(); - ft.add(new Peer(), PEER_TAG); - ft.commit(); - act.getFragmentManager().executePendingTransactions(); - } - }); - } - - boolean isChromeOS() { - return getPackageManager().hasSystemFeature("android.hardware.type.pc"); - } - - void prepareVPN(Activity act, int reqCode) { - act.runOnUiThread(new Runnable() { - @Override public void run() { - Intent intent = VpnService.prepare(act); - if (intent == null) { - onVPNPrepared(); - } else { - startActivityForResult(act, intent, reqCode); - } - } - }); - } - - static void startActivityForResult(Activity act, Intent intent, int request) { - Fragment f = act.getFragmentManager().findFragmentByTag(PEER_TAG); - f.startActivityForResult(intent, request); - } - - void showURL(Activity act, String url) { - act.runOnUiThread(new Runnable() { - @Override public void run() { - CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); - int headerColor = 0xff496495; - builder.setToolbarColor(headerColor); - CustomTabsIntent intent = builder.build(); - intent.launchUrl(act, Uri.parse(url)); - } - }); - } - - // getPackageSignatureFingerprint returns the first package signing certificate, if any. - byte[] getPackageCertificate() throws Exception { - PackageInfo info; - info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES); - for (Signature signature : info.signatures) { - return signature.toByteArray(); - } - return null; - } - - void requestNotificationPermission(Activity act) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - // We can send notifications without explicit notifications permission. - return; - } - if (ContextCompat.checkSelfPermission(act, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - return; - } - act.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, IPNActivity.NOTIFICATIONS_PERMISSION_RESULT); - } - - void requestWriteStoragePermission(Activity act) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - // We can write files without permission. - return; - } - if (ContextCompat.checkSelfPermission(act, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { - return; - } - act.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, IPNActivity.WRITE_STORAGE_RESULT); - } - - String insertMedia(String name, String mimeType) throws IOException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ContentResolver resolver = getContentResolver(); - ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name); - if (!"".equals(mimeType)) { - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); - } - Uri root = MediaStore.Files.getContentUri("external"); - return resolver.insert(root, contentValues).toString(); - } else { - File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - dir.mkdirs(); - File f = new File(dir, name); - return Uri.fromFile(f).toString(); - } - } - - int openUri(String uri, String mode) throws IOException { - ContentResolver resolver = getContentResolver(); - return resolver.openFileDescriptor(Uri.parse(uri), mode).detachFd(); - } - - void deleteUri(String uri) { - ContentResolver resolver = getContentResolver(); - resolver.delete(Uri.parse(uri), null, null); - } - - public void notifyFile(String uri, String msg) { - Intent viewIntent; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); - } else { - // uri is a file:// which is not allowed to be shared outside the app. - viewIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); - } - PendingIntent pending = PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT); - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, FILE_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("File received") - .setContentText(msg) - .setContentIntent(pending) - .setAutoCancel(true) - .setOnlyAlertOnce(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT); - - NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.notify(FILE_NOTIFICATION_ID, builder.build()); - } - - public void createNotificationChannel(String id, String name, int importance) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return; - } - NotificationChannel channel = new NotificationChannel(id, name, importance); - NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.createNotificationChannel(channel); - } - - static native void onVPNPrepared(); - private static native void onDnsConfigChanged(); - static native void onShareIntent(int nfiles, int[] types, String[] mimes, String[] items, String[] names, long[] sizes); - static native void onWriteStorageGranted(); - - // 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; - String getInterfacesAsString() { - List interfaces; - try { - interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); - } catch (Exception e) { - return ""; - } - - StringBuilder sb = new StringBuilder(""); - for (NetworkInterface nif : 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.getName(), - nif.getIndex(), nif.getMTU(), nif.isUp(), nif.supportsMulticast(), - nif.isLoopback(), nif.isPointToPoint(), nif.supportsMulticast())); - - for (InterfaceAddress ia : nif.getInterfaceAddresses()) { - // InterfaceAddress == hostname + "/" + IP - String[] parts = ia.toString().split("/", 0); - if (parts.length > 1) { - sb.append(String.format(java.util.Locale.ROOT, "%s/%d ", parts[1], ia.getNetworkPrefixLength())); - } - } - } catch (Exception e) { - // TODO(dgentry) should log the exception not silently suppress it. - continue; - } - sb.append("\n"); - } - - return sb.toString(); - } - - boolean isTV() { - UiModeManager mm = (UiModeManager)getSystemService(UI_MODE_SERVICE); - return mm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; - } -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/DnsConfig.java b/android_legacy/src/main/java/com/tailscale/ipn/DnsConfig.java deleted file mode 100644 index 19209de..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/DnsConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.net.NetworkCapabilities; -import android.net.NetworkRequest; - -import java.lang.reflect.Method; - -import java.net.InetAddress; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -// Tailscale DNS Config retrieval -// -// Tailscale's DNS support can either override the local DNS servers with a set of servers -// configured in the admin panel, or supplement the local DNS servers with additional -// servers for specific domains like example.com.beta.tailscale.net. In the non-override mode, -// we need to retrieve the current set of DNS servers from the platform. These will typically -// be the DNS servers received from DHCP. -// -// Importantly, after the Tailscale VPN comes up it will set a DNS server of 100.100.100.100 -// but we still want to retrieve the underlying DNS servers received from DHCP. If we roam -// from Wi-Fi to LTE, we want the DNS servers received from LTE. - -public class DnsConfig { - private String dnsConfigs; - - // getDnsConfigAsString returns the current DNS configuration as a multiline string: - // line[0] DNS server addresses separated by spaces - // line[1] search domains separated by spaces - // - // For example: - // 8.8.8.8 8.8.4.4 - // example.com - // - // an empty string means the current DNS configuration could not be retrieved. - String getDnsConfigAsString() { - return getDnsConfigs().trim(); - } - - private String getDnsConfigs(){ - synchronized(this) { - return this.dnsConfigs; - } - } - - void updateDNSFromNetwork(String dnsConfigs){ - synchronized(this) { - this.dnsConfigs = dnsConfigs; - } - } - - NetworkRequest getDNSConfigNetworkRequest(){ - // Request networks that are able to reach the Internet. - return new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build(); - } -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/IPNActivity.java b/android_legacy/src/main/java/com/tailscale/ipn/IPNActivity.java deleted file mode 100644 index 544558b..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/IPNActivity.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.app.Activity; -import android.content.res.AssetFileDescriptor; -import android.content.res.Configuration; -import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; -import android.provider.OpenableColumns; -import android.net.Uri; -import android.content.pm.PackageManager; - -import java.util.List; -import java.util.ArrayList; - -import org.gioui.GioView; - -public final class IPNActivity extends Activity { - final static int WRITE_STORAGE_RESULT = 1000; - final static int NOTIFICATIONS_PERMISSION_RESULT = 1001; - - private GioView view; - - @Override public void onCreate(Bundle state) { - super.onCreate(state); - view = new GioView(this); - setContentView(view); - handleIntent(); - } - - @Override public void onNewIntent(Intent i) { - setIntent(i); - handleIntent(); - } - - private void handleIntent() { - Intent it = getIntent(); - String act = it.getAction(); - String[] texts; - Uri[] uris; - if (Intent.ACTION_SEND.equals(act)) { - uris = new Uri[]{it.getParcelableExtra(Intent.EXTRA_STREAM)}; - texts = new String[]{it.getStringExtra(Intent.EXTRA_TEXT)}; - } else if (Intent.ACTION_SEND_MULTIPLE.equals(act)) { - List extraUris = it.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - uris = extraUris.toArray(new Uri[0]); - texts = new String[uris.length]; - } else { - return; - } - String mime = it.getType(); - int nitems = uris.length; - String[] items = new String[nitems]; - String[] mimes = new String[nitems]; - int[] types = new int[nitems]; - String[] names = new String[nitems]; - long[] sizes = new long[nitems]; - int nfiles = 0; - for (int i = 0; i < uris.length; i++) { - String text = texts[i]; - Uri uri = uris[i]; - if (text != null) { - types[nfiles] = 1; // FileTypeText - names[nfiles] = "file.txt"; - mimes[nfiles] = mime; - items[nfiles] = text; - // Determined by len(text) in Go to eliminate UTF-8 encoding differences. - sizes[nfiles] = 0; - nfiles++; - } else if (uri != null) { - Cursor c = getContentResolver().query(uri, null, null, null, null); - if (c == null) { - // Ignore files we have no permission to access. - continue; - } - int nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME); - int sizeCol = c.getColumnIndex(OpenableColumns.SIZE); - c.moveToFirst(); - String name = c.getString(nameCol); - long size = c.getLong(sizeCol); - types[nfiles] = 2; // FileTypeURI - mimes[nfiles] = mime; - items[nfiles] = uri.toString(); - names[nfiles] = name; - sizes[nfiles] = size; - nfiles++; - } - } - App.onShareIntent(nfiles, types, mimes, items, names, sizes); - } - - @Override public void onRequestPermissionsResult(int reqCode, String[] perms, int[] grants) { - switch (reqCode) { - case WRITE_STORAGE_RESULT: - if (grants.length > 0 && grants[0] == PackageManager.PERMISSION_GRANTED) { - App.onWriteStorageGranted(); - } - break; - case NOTIFICATIONS_PERMISSION_RESULT: - // Start the VPN regardless of the notifications permission being granted. - // It's not a blocker for running the VPN. - App app = ((App)getApplicationContext()); - app.startVPN(); - break; - } - } - - @Override public void onDestroy() { - view.destroy(); - super.onDestroy(); - } - - @Override public void onStart() { - super.onStart(); - view.start(); - } - - @Override public void onStop() { - view.stop(); - super.onStop(); - } - - @Override public void onConfigurationChanged(Configuration c) { - super.onConfigurationChanged(c); - view.configurationChanged(); - } - - @Override public void onLowMemory() { - super.onLowMemory(); - view.onLowMemory(); - } - - @Override public void onBackPressed() { - if (!view.backPressed()) - super.onBackPressed(); - } -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/IPNReceiver.java b/android_legacy/src/main/java/com/tailscale/ipn/IPNReceiver.java deleted file mode 100644 index a299500..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/IPNReceiver.java +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import androidx.work.WorkManager; -import androidx.work.OneTimeWorkRequest; - -public class IPNReceiver extends BroadcastReceiver { - - public static final String INTENT_CONNECT_VPN = "com.tailscale.ipn.CONNECT_VPN"; - public static final String INTENT_DISCONNECT_VPN = "com.tailscale.ipn.DISCONNECT_VPN"; - - @Override - public void onReceive(Context context, Intent intent) { - WorkManager workManager = WorkManager.getInstance(context); - - // On the relevant action, start the relevant worker, which can stay active for longer than this receiver can. - if (intent.getAction() == INTENT_CONNECT_VPN) { - workManager.enqueue(new OneTimeWorkRequest.Builder(StartVPNWorker.class).build()); - } else if (intent.getAction() == INTENT_DISCONNECT_VPN) { - workManager.enqueue(new OneTimeWorkRequest.Builder(StopVPNWorker.class).build()); - } - } -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/IPNService.java b/android_legacy/src/main/java/com/tailscale/ipn/IPNService.java deleted file mode 100644 index 9c4dbb1..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/IPNService.java +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.os.Build; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.VpnService; -import android.system.OsConstants; -import androidx.work.WorkManager; -import androidx.work.OneTimeWorkRequest; - -import org.gioui.GioActivity; - -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -public class IPNService extends VpnService { - public static final String ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN"; - public static final String ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN"; - - @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null && ACTION_STOP_VPN.equals(intent.getAction())) { - ((App)getApplicationContext()).autoConnect = false; - close(); - return START_NOT_STICKY; - } - if (intent != null && "android.net.VpnService".equals(intent.getAction())) { - // Start VPN and connect to it due to Always-on VPN - Intent i = new Intent(IPNReceiver.INTENT_CONNECT_VPN); - i.setPackage(getPackageName()); - i.setClass(getApplicationContext(), com.tailscale.ipn.IPNReceiver.class); - sendBroadcast(i); - requestVPN(); - connect(); - return START_STICKY; - } - requestVPN(); - App app = ((App)getApplicationContext()); - if (app.vpnReady && app.autoConnect) { - connect(); - } - return START_STICKY; - } - - private void close() { - stopForeground(true); - disconnect(); - } - - @Override public void onDestroy() { - close(); - super.onDestroy(); - } - - @Override public void onRevoke() { - close(); - super.onRevoke(); - } - - private PendingIntent configIntent() { - return PendingIntent.getActivity(this, 0, new Intent(this, IPNActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } - - private void disallowApp(VpnService.Builder b, String name) { - try { - b.addDisallowedApplication(name); - } catch (PackageManager.NameNotFoundException e) { - return; - } - } - - protected VpnService.Builder newBuilder() { - VpnService.Builder b = new VpnService.Builder() - .setConfigureIntent(configIntent()) - .allowFamily(OsConstants.AF_INET) - .allowFamily(OsConstants.AF_INET6); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - b.setMetered(false); // Inherit the metered status from the underlying networks. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - b.setUnderlyingNetworks(null); // Use all available networks. - - // RCS/Jibe https://github.com/tailscale/tailscale/issues/2322 - this.disallowApp(b, "com.google.android.apps.messaging"); - - // Stadia https://github.com/tailscale/tailscale/issues/3460 - this.disallowApp(b, "com.google.stadia.android"); - - // Android Auto https://github.com/tailscale/tailscale/issues/3828 - this.disallowApp(b, "com.google.android.projection.gearhead"); - - // GoPro https://github.com/tailscale/tailscale/issues/2554 - this.disallowApp(b, "com.gopro.smarty"); - - // Sonos https://github.com/tailscale/tailscale/issues/2548 - this.disallowApp(b, "com.sonos.acr"); - this.disallowApp(b, "com.sonos.acr2"); - - // Google Chromecast https://github.com/tailscale/tailscale/issues/3636 - this.disallowApp(b, "com.google.android.apps.chromecast.app"); - - return b; - } - - public void notify(String title, String message) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(title) - .setContentText(message) - .setContentIntent(configIntent()) - .setAutoCancel(true) - .setOnlyAlertOnce(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT); - - NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build()); - } - - public void updateStatusNotification(String title, String message) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(title) - .setContentText(message) - .setContentIntent(configIntent()) - .setPriority(NotificationCompat.PRIORITY_LOW); - - startForeground(App.STATUS_NOTIFICATION_ID, builder.build()); - } - - private native void requestVPN(); - - private native void disconnect(); - private native void connect(); -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/Peer.java b/android_legacy/src/main/java/com/tailscale/ipn/Peer.java deleted file mode 100644 index a5c49e7..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/Peer.java +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.app.Activity; -import android.app.Fragment; -import android.content.Intent; - -public class Peer extends Fragment { - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - onActivityResult0(getActivity(), requestCode, resultCode); - } - - private static native void onActivityResult0(Activity act, int reqCode, int resCode); -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/QuickToggleService.java b/android_legacy/src/main/java/com/tailscale/ipn/QuickToggleService.java deleted file mode 100644 index f77a30c..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/QuickToggleService.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.content.Context; -import android.content.Intent; -import android.service.quicksettings.Tile; -import android.service.quicksettings.TileService; - -public class QuickToggleService extends TileService { - // lock protects the static fields below it. - private static Object lock = new Object(); - // Active tracks whether the VPN is active. - private static boolean active; - // Ready tracks whether the tailscale backend is - // ready to switch on/off. - private static boolean ready; - // currentTile tracks getQsTile while service is listening. - private static Tile currentTile; - - @Override public void onStartListening() { - synchronized (lock) { - currentTile = getQsTile(); - } - updateTile(); - } - - @Override public void onStopListening() { - synchronized (lock) { - currentTile = null; - } - } - - @Override public void onClick() { - boolean r; - synchronized (lock) { - r = ready; - } - if (r) { - onTileClick(); - } else { - // Start main activity. - Intent i = getPackageManager().getLaunchIntentForPackage(getPackageName()); - startActivityAndCollapse(i); - } - } - - private static void updateTile() { - Tile t; - boolean act; - synchronized (lock) { - t = currentTile; - act = active && ready; - } - if (t == null) { - return; - } - t.setState(act ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); - t.updateTile(); - } - - static void setReady(Context ctx, boolean rdy) { - synchronized (lock) { - ready = rdy; - } - updateTile(); - } - - static void setStatus(Context ctx, boolean act) { - synchronized (lock) { - active = act; - } - updateTile(); - } - - private void onTileClick() { - boolean act; - synchronized (lock) { - act = active && ready; - } - Intent i = new Intent(act ? IPNReceiver.INTENT_DISCONNECT_VPN : IPNReceiver.INTENT_CONNECT_VPN); - i.setPackage(getPackageName()); - i.setClass(getApplicationContext(), com.tailscale.ipn.IPNReceiver.class); - sendBroadcast(i); - } -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/StartVPNWorker.java b/android_legacy/src/main/java/com/tailscale/ipn/StartVPNWorker.java deleted file mode 100644 index 5defe23..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/StartVPNWorker.java +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.net.VpnService; -import android.os.Build; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -public final class StartVPNWorker extends Worker { - - public StartVPNWorker( - Context appContext, - WorkerParameters workerParams) { - super(appContext, workerParams); - } - - @Override public Result doWork() { - App app = ((App)getApplicationContext()); - - // We will start the VPN from the background - app.autoConnect = true; - // We need to make sure we prepare the VPN Service, just in case it isn't prepared. - - Intent intent = VpnService.prepare(app); - if (intent == null) { - // If null then the VPN is already prepared and/or it's just been prepared because we have permission - app.startVPN(); - return Result.success(); - } else { - // This VPN possibly doesn't have permission, we need to display a notification which when clicked launches the intent provided. - android.util.Log.e("StartVPNWorker", "Tailscale doesn't have permission from the system to start VPN. Launching the intent provided."); - - // Send notification - NotificationManager notificationManager = (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE); - String channelId = "start_vpn_channel"; - - // Use createNotificationChannel method from App.java - app.createNotificationChannel(channelId, "Start VPN Channel", NotificationManager.IMPORTANCE_DEFAULT); - - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - int pendingIntentFlags = PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0); - PendingIntent pendingIntent = PendingIntent.getActivity(app, 0, intent, pendingIntentFlags); - - Notification notification = new Notification.Builder(app, channelId) - .setContentTitle("Tailscale Connection Failed") - .setContentText("Tap here to renew permission.") - .setSmallIcon(R.drawable.ic_notification) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .build(); - - notificationManager.notify(1, notification); - - return Result.failure(); - } - } -} \ No newline at end of file diff --git a/android_legacy/src/main/java/com/tailscale/ipn/StopVPNWorker.java b/android_legacy/src/main/java/com/tailscale/ipn/StopVPNWorker.java deleted file mode 100644 index 2d2a378..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/StopVPNWorker.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import androidx.work.Worker; -import android.content.Context; -import androidx.work.WorkerParameters; - -public final class StopVPNWorker extends Worker { - - public StopVPNWorker( - Context appContext, - WorkerParameters workerParams) { - super(appContext, workerParams); - } - - @Override public Result doWork() { - disconnect(); - return Result.success(); - } - - private native void disconnect(); -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/LocalAPIClient.kt b/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/LocalAPIClient.kt deleted file mode 100644 index e065098..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/LocalAPIClient.kt +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -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) -> Unit - -typealias BugReportIdHandler = (Result) -> Unit - -typealias PrefsHandler = (Result) -> 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 executeRequest(request: LocalAPIRequest) { - 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>() - 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(req) - } - - fun getBugReportId(responseHandler: BugReportIdHandler) { - val req = LocalAPIRequest.bugReportId(responseHandler) - executeRequest(req) - } - - fun getPrefs(responseHandler: PrefsHandler) { - val req = LocalAPIRequest.prefs(responseHandler) - executeRequest(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}") - } - } - } -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/LocalAPIRequest.kt b/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/LocalAPIRequest.kt deleted file mode 100644 index 83f5705..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/LocalAPIRequest.kt +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -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( - val path: String, - val method: String, - val body: String? = null, - val responseHandler: (Result) -> 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 { - val path = LocalAPIEndpoint.Status.path() - return LocalAPIRequest(path, "GET", null, responseHandler) { resp -> - responseHandler(decode(resp)) - } - } - - fun bugReportId(responseHandler: BugReportIdHandler): LocalAPIRequest { - val path = LocalAPIEndpoint.BugReport.path() - return LocalAPIRequest(path, "POST", null, responseHandler) { resp -> - responseHandler(parseString(resp)) - } - } - - fun prefs(responseHandler: PrefsHandler): LocalAPIRequest { - val path = LocalAPIEndpoint.Prefs.path() - return LocalAPIRequest(path, "GET", null, responseHandler) { resp -> - responseHandler(decode(resp)) - } - } - - // Check if the response was a generic error - fun parseError(respData: String): Error { - try { - val err = Json.decodeFromString(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 { - 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 decode(respData: String): Result { - try { - val message = decoder.decodeFromString(respData) - return Result(message) - } catch (e: Exception) { - return Result(parseError(respData)) - } - } - } - - val cookie: String = getCookie() -} diff --git a/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/Result.kt b/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/Result.kt deleted file mode 100644 index 4180171..0000000 --- a/android_legacy/src/main/java/com/tailscale/ipn/ui/localapi/Result.kt +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -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 { - 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 -} \ No newline at end of file diff --git a/android_legacy/src/main/res/drawable-hdpi/ic_notification.png b/android_legacy/src/main/res/drawable-hdpi/ic_notification.png deleted file mode 100644 index e6aec75..0000000 Binary files a/android_legacy/src/main/res/drawable-hdpi/ic_notification.png and /dev/null differ diff --git a/android_legacy/src/main/res/drawable-mdpi/ic_notification.png b/android_legacy/src/main/res/drawable-mdpi/ic_notification.png deleted file mode 100644 index 129824e..0000000 Binary files a/android_legacy/src/main/res/drawable-mdpi/ic_notification.png and /dev/null differ diff --git a/android_legacy/src/main/res/drawable-xhdpi/ic_notification.png b/android_legacy/src/main/res/drawable-xhdpi/ic_notification.png deleted file mode 100644 index 8066819..0000000 Binary files a/android_legacy/src/main/res/drawable-xhdpi/ic_notification.png and /dev/null differ diff --git a/android_legacy/src/main/res/drawable-xhdpi/tv_banner.png b/android_legacy/src/main/res/drawable-xhdpi/tv_banner.png deleted file mode 100644 index b5543a6..0000000 Binary files a/android_legacy/src/main/res/drawable-xhdpi/tv_banner.png and /dev/null differ diff --git a/android_legacy/src/main/res/drawable-xxhdpi/ic_notification.png b/android_legacy/src/main/res/drawable-xxhdpi/ic_notification.png deleted file mode 100644 index 615f569..0000000 Binary files a/android_legacy/src/main/res/drawable-xxhdpi/ic_notification.png and /dev/null differ diff --git a/android_legacy/src/main/res/drawable-xxxhdpi/ic_notification.png b/android_legacy/src/main/res/drawable-xxxhdpi/ic_notification.png deleted file mode 100644 index a45d73c..0000000 Binary files a/android_legacy/src/main/res/drawable-xxxhdpi/ic_notification.png and /dev/null differ diff --git a/android_legacy/src/main/res/drawable/ic_launcher_foreground.xml b/android_legacy/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 154dbef..0000000 --- a/android_legacy/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - diff --git a/android_legacy/src/main/res/drawable/ic_tile.xml b/android_legacy/src/main/res/drawable/ic_tile.xml deleted file mode 100644 index 3cd5907..0000000 --- a/android_legacy/src/main/res/drawable/ic_tile.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - diff --git a/android_legacy/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android_legacy/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 7353dbd..0000000 --- a/android_legacy/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android_legacy/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android_legacy/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 7353dbd..0000000 --- a/android_legacy/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android_legacy/src/main/res/mipmap-hdpi/ic_launcher.png b/android_legacy/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index b5f0466..0000000 Binary files a/android_legacy/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android_legacy/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 91ad4fc..0000000 Binary files a/android_legacy/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-mdpi/ic_launcher.png b/android_legacy/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 84001c9..0000000 Binary files a/android_legacy/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android_legacy/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index ca60a18..0000000 Binary files a/android_legacy/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-xhdpi/ic_launcher.png b/android_legacy/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index a9a1919..0000000 Binary files a/android_legacy/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android_legacy/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index e1e7216..0000000 Binary files a/android_legacy/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android_legacy/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d3408ec..0000000 Binary files a/android_legacy/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android_legacy/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index d222f31..0000000 Binary files a/android_legacy/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android_legacy/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 40521d4..0000000 Binary files a/android_legacy/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android_legacy/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android_legacy/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index e620f35..0000000 Binary files a/android_legacy/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android_legacy/src/main/res/values/ic_launcher_background.xml b/android_legacy/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index 16b8946..0000000 --- a/android_legacy/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #1F2125 - \ No newline at end of file diff --git a/android_legacy/src/main/res/values/strings.xml b/android_legacy/src/main/res/values/strings.xml deleted file mode 100644 index 07b3455..0000000 --- a/android_legacy/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - Tailscale - Tailscale - diff --git a/android_legacy/src/play/java/com/tailscale/ipn/Google.java b/android_legacy/src/play/java/com/tailscale/ipn/Google.java deleted file mode 100644 index 44a4787..0000000 --- a/android_legacy/src/play/java/com/tailscale/ipn/Google.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn; - -import android.app.Activity; -import android.content.Intent; -import android.content.Context; - -import com.google.android.gms.auth.api.signin.GoogleSignIn; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.auth.api.signin.GoogleSignInClient; -import com.google.android.gms.auth.api.signin.GoogleSignInOptions; - -// Google implements helpers for Google services. -public final class Google { - static String getIdTokenForActivity(Activity act) { - GoogleSignInAccount acc = GoogleSignIn.getLastSignedInAccount(act); - return acc.getIdToken(); - } - - static void googleSignIn(Activity act, String serverOAuthID, int reqCode) { - act.runOnUiThread(new Runnable() { - @Override public void run() { - GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(serverOAuthID) - .requestEmail() - .build(); - GoogleSignInClient client = GoogleSignIn.getClient(act, gso); - Intent signInIntent = client.getSignInIntent(); - App.startActivityForResult(act, signInIntent, reqCode); - } - }); - } - - static void googleSignOut(Context ctx) { - GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .build(); - GoogleSignInClient client = GoogleSignIn.getClient(ctx, gso); - client.signOut(); - } -} diff --git a/cmd/cmd.iml b/cmd/cmd.iml deleted file mode 100644 index 8021953..0000000 --- a/cmd/cmd.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/cmd/jni/jni.go b/cmd/jni/jni.go deleted file mode 100644 index 0cfcb49..0000000 --- a/cmd/jni/jni.go +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -// Package jni implements various helper functions for communicating with the Android JVM -// though JNI. -package jni - -import ( - "errors" - "fmt" - "reflect" - "runtime" - "sync" - "unicode/utf16" - "unsafe" -) - -/* -#cgo CFLAGS: -Wall - -#include -#include - -static jint jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) { - return (*vm)->AttachCurrentThread(vm, p_env, thr_args); -} - -static jint jni_DetachCurrentThread(JavaVM *vm) { - return (*vm)->DetachCurrentThread(vm); -} - -static jint jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) { - return (*vm)->GetEnv(vm, (void **)env, version); -} - -static jclass jni_FindClass(JNIEnv *env, const char *name) { - return (*env)->FindClass(env, name); -} - -static jthrowable jni_ExceptionOccurred(JNIEnv *env) { - return (*env)->ExceptionOccurred(env); -} - -static void jni_ExceptionClear(JNIEnv *env) { - (*env)->ExceptionClear(env); -} - -static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) { - return (*env)->GetObjectClass(env, obj); -} - -static jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { - return (*env)->GetMethodID(env, clazz, name, sig); -} - -static jmethodID jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { - return (*env)->GetStaticMethodID(env, clazz, name, sig); -} - -static jsize jni_GetStringLength(JNIEnv *env, jstring str) { - return (*env)->GetStringLength(env, str); -} - -static const jchar *jni_GetStringChars(JNIEnv *env, jstring str) { - return (*env)->GetStringChars(env, str, NULL); -} - -static jstring jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) { - return (*env)->NewString(env, unicodeChars, len); -} - -static jboolean jni_IsSameObject(JNIEnv *env, jobject ref1, jobject ref2) { - return (*env)->IsSameObject(env, ref1, ref2); -} - -static jobject jni_NewGlobalRef(JNIEnv *env, jobject obj) { - return (*env)->NewGlobalRef(env, obj); -} - -static void jni_DeleteGlobalRef(JNIEnv *env, jobject obj) { - (*env)->DeleteGlobalRef(env, obj); -} - -static void jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { - (*env)->CallStaticVoidMethodA(env, cls, method, args); -} - -static jint jni_CallStaticIntMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { - return (*env)->CallStaticIntMethodA(env, cls, method, args); -} - -static jobject jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { - return (*env)->CallStaticObjectMethodA(env, cls, method, args); -} - -static jobject jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { - return (*env)->CallObjectMethodA(env, obj, method, args); -} - -static jboolean jni_CallBooleanMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { - return (*env)->CallBooleanMethodA(env, obj, method, args); -} - -static jint jni_CallIntMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { - return (*env)->CallIntMethodA(env, obj, method, args); -} - -static void jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { - (*env)->CallVoidMethodA(env, obj, method, args); -} - -static jbyteArray jni_NewByteArray(JNIEnv *env, jsize length) { - return (*env)->NewByteArray(env, length); -} - -static jboolean *jni_GetBooleanArrayElements(JNIEnv *env, jbooleanArray arr) { - return (*env)->GetBooleanArrayElements(env, arr, NULL); -} - -static void jni_ReleaseBooleanArrayElements(JNIEnv *env, jbooleanArray arr, jboolean *elems, jint mode) { - (*env)->ReleaseBooleanArrayElements(env, arr, elems, mode); -} - -static jbyte *jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) { - return (*env)->GetByteArrayElements(env, arr, NULL); -} - -static jint *jni_GetIntArrayElements(JNIEnv *env, jintArray arr) { - return (*env)->GetIntArrayElements(env, arr, NULL); -} - -static void jni_ReleaseIntArrayElements(JNIEnv *env, jintArray arr, jint *elems, jint mode) { - (*env)->ReleaseIntArrayElements(env, arr, elems, mode); -} - -static jlong *jni_GetLongArrayElements(JNIEnv *env, jlongArray arr) { - return (*env)->GetLongArrayElements(env, arr, NULL); -} - -static void jni_ReleaseLongArrayElements(JNIEnv *env, jlongArray arr, jlong *elems, jint mode) { - (*env)->ReleaseLongArrayElements(env, arr, elems, mode); -} - -static void jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *elems, jint mode) { - (*env)->ReleaseByteArrayElements(env, arr, elems, mode); -} - -static jsize jni_GetArrayLength(JNIEnv *env, jarray arr) { - return (*env)->GetArrayLength(env, arr); -} - -static void jni_DeleteLocalRef(JNIEnv *env, jobject localRef) { - return (*env)->DeleteLocalRef(env, localRef); -} - -static jobject jni_GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index) { - return (*env)->GetObjectArrayElement(env, array, index); -} - -static jboolean jni_IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz) { - return (*env)->IsInstanceOf(env, obj, clazz); -} -*/ -import "C" - -type JVM C.JavaVM - -type Env C.JNIEnv - -type ( - Class C.jclass - Object C.jobject - MethodID C.jmethodID - String C.jstring - ByteArray C.jbyteArray - ObjectArray C.jobjectArray - BooleanArray C.jbooleanArray - LongArray C.jlongArray - IntArray C.jintArray - Boolean C.jboolean - Value uint64 // All JNI types fit into 64-bits. -) - -// Cached class handles. -var classes struct { - once sync.Once - stringClass, integerClass Class - - integerIntValue MethodID -} - -func env(e *Env) *C.JNIEnv { - return (*C.JNIEnv)(unsafe.Pointer(e)) -} - -func javavm(vm *JVM) *C.JavaVM { - return (*C.JavaVM)(unsafe.Pointer(vm)) -} - -// Do invokes a function with a temporary JVM environment. The -// environment is not valid after the function returns. -func Do(vm *JVM, f func(env *Env) error) error { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - var env *C.JNIEnv - if res := C.jni_GetEnv(javavm(vm), &env, C.JNI_VERSION_1_6); res != C.JNI_OK { - if res != C.JNI_EDETACHED { - panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) - } - if C.jni_AttachCurrentThread(javavm(vm), &env, nil) != C.JNI_OK { - panic(errors.New("runInJVM: AttachCurrentThread failed")) - } - defer C.jni_DetachCurrentThread(javavm(vm)) - } - - return f((*Env)(unsafe.Pointer(env))) -} - -func Bool(b bool) Boolean { - if b { - return C.JNI_TRUE - } - return C.JNI_FALSE -} - -func varArgs(args []Value) *C.jvalue { - if len(args) == 0 { - return nil - } - return (*C.jvalue)(unsafe.Pointer(&args[0])) -} - -func IsSameObject(e *Env, ref1, ref2 Object) bool { - same := C.jni_IsSameObject(env(e), C.jobject(ref1), C.jobject(ref2)) - return same == C.JNI_TRUE -} - -func CallStaticIntMethod(e *Env, cls Class, method MethodID, args ...Value) (int, error) { - res := C.jni_CallStaticIntMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args)) - return int(res), exception(e) -} - -func CallStaticVoidMethod(e *Env, cls Class, method MethodID, args ...Value) error { - C.jni_CallStaticVoidMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args)) - return exception(e) -} - -func CallVoidMethod(e *Env, obj Object, method MethodID, args ...Value) error { - C.jni_CallVoidMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args)) - return exception(e) -} - -func CallStaticObjectMethod(e *Env, cls Class, method MethodID, args ...Value) (Object, error) { - res := C.jni_CallStaticObjectMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args)) - return Object(res), exception(e) -} - -func CallObjectMethod(e *Env, obj Object, method MethodID, args ...Value) (Object, error) { - res := C.jni_CallObjectMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args)) - return Object(res), exception(e) -} - -func CallBooleanMethod(e *Env, obj Object, method MethodID, args ...Value) (bool, error) { - res := C.jni_CallBooleanMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args)) - return res == C.JNI_TRUE, exception(e) -} - -func CallIntMethod(e *Env, obj Object, method MethodID, args ...Value) (int32, error) { - res := C.jni_CallIntMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args)) - return int32(res), exception(e) -} - -// GetByteArrayElements returns the contents of the byte array. -func GetByteArrayElements(e *Env, jarr ByteArray) []byte { - if jarr == 0 { - return nil - } - size := C.jni_GetArrayLength(env(e), C.jarray(jarr)) - elems := C.jni_GetByteArrayElements(env(e), C.jbyteArray(jarr)) - defer C.jni_ReleaseByteArrayElements(env(e), C.jbyteArray(jarr), elems, 0) - backing := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:size:size] - s := make([]byte, len(backing)) - copy(s, backing) - return s -} - -// GetBooleanArrayElements returns the contents of the boolean array. -func GetBooleanArrayElements(e *Env, jarr BooleanArray) []bool { - if jarr == 0 { - return nil - } - size := C.jni_GetArrayLength(env(e), C.jarray(jarr)) - elems := C.jni_GetBooleanArrayElements(env(e), C.jbooleanArray(jarr)) - defer C.jni_ReleaseBooleanArrayElements(env(e), C.jbooleanArray(jarr), elems, 0) - backing := (*(*[1 << 30]C.jboolean)(unsafe.Pointer(elems)))[:size:size] - r := make([]bool, len(backing)) - for i, b := range backing { - r[i] = b == C.JNI_TRUE - } - return r -} - -// GetStringArrayElements returns the contents of the String array. -func GetStringArrayElements(e *Env, jarr ObjectArray) []string { - var strings []string - iterateObjectArray(e, jarr, func(e *Env, idx int, item Object) { - s := GoString(e, String(item)) - strings = append(strings, s) - }) - return strings -} - -// GetIntArrayElements returns the contents of the int array. -func GetIntArrayElements(e *Env, jarr IntArray) []int { - if jarr == 0 { - return nil - } - size := C.jni_GetArrayLength(env(e), C.jarray(jarr)) - elems := C.jni_GetIntArrayElements(env(e), C.jintArray(jarr)) - defer C.jni_ReleaseIntArrayElements(env(e), C.jintArray(jarr), elems, 0) - backing := (*(*[1 << 27]C.jint)(unsafe.Pointer(elems)))[:size:size] - r := make([]int, len(backing)) - for i, l := range backing { - r[i] = int(l) - } - return r -} - -// GetLongArrayElements returns the contents of the long array. -func GetLongArrayElements(e *Env, jarr LongArray) []int64 { - if jarr == 0 { - return nil - } - size := C.jni_GetArrayLength(env(e), C.jarray(jarr)) - elems := C.jni_GetLongArrayElements(env(e), C.jlongArray(jarr)) - defer C.jni_ReleaseLongArrayElements(env(e), C.jlongArray(jarr), elems, 0) - backing := (*(*[1 << 27]C.jlong)(unsafe.Pointer(elems)))[:size:size] - r := make([]int64, len(backing)) - for i, l := range backing { - r[i] = int64(l) - } - return r -} - -func iterateObjectArray(e *Env, jarr ObjectArray, f func(e *Env, idx int, item Object)) { - if jarr == 0 { - return - } - size := C.jni_GetArrayLength(env(e), C.jarray(jarr)) - for i := 0; i < int(size); i++ { - item := C.jni_GetObjectArrayElement(env(e), C.jobjectArray(jarr), C.jint(i)) - f(e, i, Object(item)) - C.jni_DeleteLocalRef(env(e), item) - } -} - -// NewByteArray allocates a Java byte array with the content. It -// panics if the allocation fails. -func NewByteArray(e *Env, content []byte) ByteArray { - jarr := C.jni_NewByteArray(env(e), C.jsize(len(content))) - if jarr == 0 { - panic(fmt.Errorf("jni: NewByteArray(%d) failed", len(content))) - } - elems := C.jni_GetByteArrayElements(env(e), jarr) - defer C.jni_ReleaseByteArrayElements(env(e), jarr, elems, 0) - backing := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:len(content):len(content)] - copy(backing, content) - return ByteArray(jarr) -} - -// ClassLoader returns a reference to the Java ClassLoader associated -// with obj. -func ClassLoaderFor(e *Env, obj Object) Object { - cls := GetObjectClass(e, obj) - getClassLoader := GetMethodID(e, cls, "getClassLoader", "()Ljava/lang/ClassLoader;") - clsLoader, err := CallObjectMethod(e, Object(obj), getClassLoader) - if err != nil { - // Class.getClassLoader should never fail. - panic(err) - } - return Object(clsLoader) -} - -// LoadClass invokes the underlying ClassLoader's loadClass method and -// returns the class. -func LoadClass(e *Env, loader Object, class string) (Class, error) { - cls := GetObjectClass(e, loader) - loadClass := GetMethodID(e, cls, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;") - name := JavaString(e, class) - loaded, err := CallObjectMethod(e, loader, loadClass, Value(name)) - if err != nil { - return 0, err - } - return Class(loaded), exception(e) -} - -// exception returns an error corresponding to the pending -// exception, and clears it. exceptionError returns nil if no -// exception is pending. -func exception(e *Env) error { - thr := C.jni_ExceptionOccurred(env(e)) - if thr == 0 { - return nil - } - C.jni_ExceptionClear(env(e)) - cls := GetObjectClass(e, Object(thr)) - toString := GetMethodID(e, cls, "toString", "()Ljava/lang/String;") - msg, err := CallObjectMethod(e, Object(thr), toString) - if err != nil { - return err - } - return errors.New(GoString(e, String(msg))) -} - -// GetObjectClass returns the Java Class for an Object. -func GetObjectClass(e *Env, obj Object) Class { - if obj == 0 { - panic("null object") - } - cls := C.jni_GetObjectClass(env(e), C.jobject(obj)) - if err := exception(e); err != nil { - // GetObjectClass should never fail. - panic(err) - } - return Class(cls) -} - -// GetStaticMethodID returns the id for a static method. It panics if the method -// wasn't found. -func GetStaticMethodID(e *Env, cls Class, name, signature string) MethodID { - mname := C.CString(name) - defer C.free(unsafe.Pointer(mname)) - msig := C.CString(signature) - defer C.free(unsafe.Pointer(msig)) - m := C.jni_GetStaticMethodID(env(e), C.jclass(cls), mname, msig) - if err := exception(e); err != nil { - panic(err) - } - return MethodID(m) -} - -// GetMethodID returns the id for a method. It panics if the method -// wasn't found. -func GetMethodID(e *Env, cls Class, name, signature string) MethodID { - mname := C.CString(name) - defer C.free(unsafe.Pointer(mname)) - msig := C.CString(signature) - defer C.free(unsafe.Pointer(msig)) - m := C.jni_GetMethodID(env(e), C.jclass(cls), mname, msig) - if err := exception(e); err != nil { - panic(err) - } - return MethodID(m) -} - -func NewGlobalRef(e *Env, obj Object) Object { - return Object(C.jni_NewGlobalRef(env(e), C.jobject(obj))) -} - -func DeleteGlobalRef(e *Env, obj Object) { - C.jni_DeleteGlobalRef(env(e), C.jobject(obj)) -} - -// JavaString converts the string to a JVM jstring. -func JavaString(e *Env, str string) String { - if str == "" { - return 0 - } - utf16Chars := utf16.Encode([]rune(str)) - res := C.jni_NewString(env(e), (*C.jchar)(unsafe.Pointer(&utf16Chars[0])), C.int(len(utf16Chars))) - return String(res) -} - -// GoString converts the JVM jstring to a Go string. -func GoString(e *Env, str String) string { - if str == 0 { - return "" - } - strlen := C.jni_GetStringLength(env(e), C.jstring(str)) - chars := C.jni_GetStringChars(env(e), C.jstring(str)) - var utf16Chars []uint16 - hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars)) - hdr.Data = uintptr(unsafe.Pointer(chars)) - hdr.Cap = int(strlen) - hdr.Len = int(strlen) - utf8 := utf16.Decode(utf16Chars) - return string(utf8) -} diff --git a/cmd/localapiservice/localapi_test.go b/cmd/localapiservice/localapi_test.go deleted file mode 100644 index 97f0d06..0000000 --- a/cmd/localapiservice/localapi_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package localapiservice - -import ( - "context" - "io" - "net/http" - "testing" - "time" -) - -var ctx = context.Background() - -type BadStatusHandler struct{} - -func (b *BadStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) -} - -func TestBadStatus(t *testing.T) { - ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second)) - client := New(&BadStatusHandler{}) - defer cancel() - - _, err := client.Call(ctx, "POST", "test", nil) - - if err.Error() != "request failed with status code 400" { - t.Error("Expected bad status error, but got", err) - } -} - -type TimeoutHandler struct{} - -var successfulResponse = "successful response!" - -func (b *TimeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - time.Sleep(6 * time.Second) - w.Write([]byte(successfulResponse)) -} - -func TestTimeout(t *testing.T) { - ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second)) - client := New(&TimeoutHandler{}) - defer cancel() - - _, err := client.Call(ctx, "GET", "test", nil) - - if err.Error() != "timeout for test" { - t.Error("Expected timeout error, but got", err) - } -} - -type SuccessfulHandler struct{} - -func (b *SuccessfulHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(successfulResponse)) -} - -func TestSuccess(t *testing.T) { - ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second)) - client := New(&SuccessfulHandler{}) - defer cancel() - - w, err := client.Call(ctx, "GET", "test", nil) - - if err != nil { - t.Error("Expected no error, but got", err) - } - - report, err := io.ReadAll(w.Body()) - if string(report) != successfulResponse { - t.Error("Expected successful report, but got", report) - } -} diff --git a/cmd/localapiservice/localapiservice.go b/cmd/localapiservice/localapiservice.go deleted file mode 100644 index 6fa88f5..0000000 --- a/cmd/localapiservice/localapiservice.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package localapiservice - -import ( - "context" - "fmt" - "io" - "log" - "net" - "net/http" - "time" - - "tailscale.com/ipn/ipnlocal" -) - -type LocalAPIService struct { - h http.Handler -} - -func New(h http.Handler) *LocalAPIService { - return &LocalAPIService{h: h} -} - -// Call calls the given endpoint on the local API using the given HTTP method -// optionally sending the given body. It returns a Response representing the -// result of the call and an error if the call could not be completed or the -// local API returned a status code in the 400 series or greater. -// Note - Response includes a response body available from the Body method, it -// is the caller's responsibility to close this. -func (cl *LocalAPIService) Call(ctx context.Context, method, endpoint string, body io.Reader) (*Response, error) { - req, err := http.NewRequestWithContext(ctx, method, endpoint, body) - if err != nil { - return nil, fmt.Errorf("error creating new request for %s: %w", endpoint, err) - } - deadline, _ := ctx.Deadline() - pipeReader, pipeWriter := net.Pipe() - pipeReader.SetDeadline(deadline) - pipeWriter.SetDeadline(deadline) - - resp := &Response{ - headers: http.Header{}, - status: http.StatusOK, - bodyReader: pipeReader, - bodyWriter: pipeWriter, - startWritingBody: make(chan interface{}), - } - - go func() { - cl.h.ServeHTTP(resp, req) - resp.Flush() - pipeWriter.Close() - }() - - select { - case <-resp.startWritingBody: - if resp.StatusCode() >= 400 { - return resp, fmt.Errorf("request failed with status code %d", resp.StatusCode()) - } - return resp, nil - case <-ctx.Done(): - return nil, fmt.Errorf("timeout for %s", endpoint) - } -} - -func (s *LocalAPIService) GetBugReportID(ctx context.Context, bugReportChan chan<- string, fallbackLog string) { - ctx, cancel := context.WithTimeout(ctx, 2*time.Second) - defer cancel() - r, err := s.Call(ctx, "POST", "/localapi/v0/bugreport", nil) - defer r.Body().Close() - - if err != nil { - log.Printf("get bug report: %s", err) - bugReportChan <- fallbackLog - return - } - logBytes, err := io.ReadAll(r.Body()) - if err != nil { - log.Printf("read bug report: %s", err) - bugReportChan <- fallbackLog - return - } - bugReportChan <- string(logBytes) -} - -func (s *LocalAPIService) Login(ctx context.Context, backend *ipnlocal.LocalBackend) { - ctx, cancel := context.WithTimeout(ctx, 60*time.Second) - defer cancel() - r, err := s.Call(ctx, "POST", "/localapi/v0/login-interactive", nil) - defer r.Body().Close() - - if err != nil { - log.Printf("login: %s", err) - backend.StartLoginInteractive() - } -} - -func (s *LocalAPIService) Logout(ctx context.Context, backend *ipnlocal.LocalBackend) error { - ctx, cancel := context.WithTimeout(ctx, 60*time.Second) - defer cancel() - r, err := s.Call(ctx, "POST", "/localapi/v0/logout", nil) - defer r.Body().Close() - - if err != nil { - log.Printf("logout: %s", err) - logoutctx, logoutcancel := context.WithTimeout(ctx, 5*time.Minute) - defer logoutcancel() - backend.Logout(logoutctx) - } - - return err -} diff --git a/cmd/localapiservice/localapishim.go b/cmd/localapiservice/localapishim.go deleted file mode 100644 index 974b107..0000000 --- a/cmd/localapiservice/localapishim.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package localapiservice - -import ( - "bytes" - "context" - "encoding/json" - "io" - "log" - "runtime/debug" - "time" - "unsafe" - - "github.com/tailscale/tailscale-android/cmd/jni" - "tailscale.com/ipn" - "tailscale.com/ipn/ipnlocal" -) - -// #include -import "C" - -// Shims the LocalApiClient class from the Kotlin side to the Go side's LocalAPIService. -var shim struct { - // localApiClient is a global reference to the com.tailscale.ipn.ui.localapi.LocalApiClient class. - clientClass jni.Class - - // notifierClass is a global reference to the com.tailscale.ipn.ui.notifier.Notifier class. - notifierClass jni.Class - - // Typically a shared LocalAPIService instance. - service *LocalAPIService - - backend *ipnlocal.LocalBackend - - cancelWatchBus func() - - jvm *jni.JVM -} - -//export Java_com_tailscale_ipn_ui_localapi_Request_doRequest -func Java_com_tailscale_ipn_ui_localapi_Request_doRequest( - env *C.JNIEnv, - cls C.jclass, - jmethod C.jstring, - jpath C.jstring, - jbody C.jbyteArray) { - - defer func() { - if p := recover(); p != nil { - log.Printf("doRequest() panicked with %q, stack: %s", p, debug.Stack()) - panic(p) - } - }() - - jenv := (*jni.Env)(unsafe.Pointer(env)) - - // The HTTP verb - methodRef := jni.NewGlobalRef(jenv, jni.Object(jmethod)) - methodStr := jni.GoString(jenv, jni.String(methodRef)) - defer jni.DeleteGlobalRef(jenv, methodRef) - - // The API Path - pathRef := jni.NewGlobalRef(jenv, jni.Object(jpath)) - pathStr := jni.GoString(jenv, jni.String(pathRef)) - defer jni.DeleteGlobalRef(jenv, pathRef) - - // The body string. This is optional and may be empty. - bodyRef := jni.NewGlobalRef(jenv, jni.Object(jbody)) - bodyArray := jni.GetByteArrayElements(jenv, jni.ByteArray(bodyRef)) - defer jni.DeleteGlobalRef(jenv, bodyRef) - - resp := doLocalAPIRequest(pathStr, methodStr, bodyArray) - - jrespBody := jni.NewByteArray(jenv, resp) - respBody := jni.Value(jrespBody) - onResponse := jni.GetMethodID(jenv, shim.clientClass, "onResponse", "([B)V") - - jni.CallVoidMethod(jenv, jni.Object(cls), onResponse, respBody) -} - -func doLocalAPIRequest(path string, method string, body []byte) []byte { - if shim.service == nil { - return []byte("{\"error\":\"Not Ready\"}") - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - var reader io.Reader = nil - if len(body) > 0 { - reader = bytes.NewReader(body) - } - - r, err := shim.service.Call(ctx, method, path, reader) - if err != nil { - log.Printf("error calling %s %q: %s", method, path, err) - return []byte("{\"error\":\"" + err.Error() + "\"}") - } - - defer r.Body().Close() - respBytes, err := io.ReadAll(r.Body()) - if err != nil { - return []byte("{\"error\":\"" + err.Error() + "\"}") - } - return respBytes -} - -// Assign a localAPIService to our shim for handling incoming localapi requests from the Kotlin side. -func ConfigureShim(jvm *jni.JVM, appCtx jni.Object, s *LocalAPIService, b *ipnlocal.LocalBackend) { - shim.service = s - shim.backend = b - - configureLocalAPIJNIHandler(jvm, appCtx) - - // Let the Kotlin side know we're ready to handle requests. - jni.Do(jvm, func(env *jni.Env) error { - onReadyAPI := jni.GetStaticMethodID(env, shim.clientClass, "onReady", "()V") - jni.CallStaticVoidMethod(env, shim.clientClass, onReadyAPI) - - onNotifyNot := jni.GetStaticMethodID(env, shim.notifierClass, "onReady", "()V") - jni.CallStaticVoidMethod(env, shim.notifierClass, onNotifyNot) - - log.Printf("LocalAPI Shim ready") - return nil - }) -} - -// Loads the Kotlin-side LocalApiClient class and stores it in a global reference. -func configureLocalAPIJNIHandler(jvm *jni.JVM, appCtx jni.Object) error { - shim.jvm = jvm - - return jni.Do(jvm, func(env *jni.Env) error { - loader := jni.ClassLoaderFor(env, appCtx) - cl, err := jni.LoadClass(env, loader, "com.tailscale.ipn.ui.localapi.Request") - if err != nil { - return err - } - shim.clientClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl))) - - cl, err = jni.LoadClass(env, loader, "com.tailscale.ipn.ui.notifier.Notifier") - if err != nil { - return err - } - shim.notifierClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl))) - - return nil - }) -} - -//export Java_com_tailscale_ipn_ui_notifier_Notifier_stopIPNBusWatcher -func Java_com_tailscale_ipn_ui_notifier_Notifier_stopIPNBusWatcher( - env *C.JNIEnv, - cls C.jclass) { - - if shim.cancelWatchBus != nil { - log.Printf("Stop watching IPN bus") - shim.cancelWatchBus() - shim.cancelWatchBus = nil - } else { - log.Printf("Not watching IPN bus, nothing to cancel") - } -} - -//export Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher -func Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher( - env *C.JNIEnv, - cls C.jclass, - jmask C.jint) { - - jenv := (*jni.Env)(unsafe.Pointer(env)) - - log.Printf("Start watching IPN bus") - - ctx, cancel := context.WithCancel(context.Background()) - shim.cancelWatchBus = cancel - opts := ipn.NotifyWatchOpt(jmask) - - shim.backend.WatchNotifications(ctx, opts, func() { - // onWatchAdded - }, func(roNotify *ipn.Notify) bool { - js, err := json.Marshal(roNotify) - if err != nil { - return true - } - jni.Do(shim.jvm, func(env *jni.Env) error { - jjson := jni.NewByteArray(jenv, js) - onNotify := jni.GetStaticMethodID(jenv, shim.notifierClass, "onNotify", "([B)V") - jni.CallStaticVoidMethod(jenv, shim.notifierClass, onNotify, jni.Value(jjson)) - return nil - }) - return true - }) - -} diff --git a/cmd/localapiservice/response.go b/cmd/localapiservice/response.go deleted file mode 100644 index 9e30ebc..0000000 --- a/cmd/localapiservice/response.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package localapiservice - -import ( - "net" - "net/http" - "sync" -) - -// Response represents the result of processing an localAPI request. -// On completion, the response body can be read out of the bodyWriter. -type Response struct { - headers http.Header - status int - bodyWriter net.Conn - bodyReader net.Conn - startWritingBody chan interface{} - startWritingBodyOnce sync.Once -} - -func (r *Response) Header() http.Header { - return r.headers -} - -// Write writes the data to the response body which an then be -// read out as a json object. -func (r *Response) Write(data []byte) (int, error) { - r.Flush() - if r.status == 0 { - r.WriteHeader(http.StatusOK) - } - return r.bodyWriter.Write(data) -} - -func (r *Response) WriteHeader(statusCode int) { - r.status = statusCode -} - -func (r *Response) Body() net.Conn { - return r.bodyReader -} - -func (r *Response) StatusCode() int { - return r.status -} - -func (r *Response) Flush() { - r.startWritingBodyOnce.Do(func() { - close(r.startWritingBody) - }) -} diff --git a/cmd/tailscale/backend.go b/cmd/tailscale/backend.go deleted file mode 100644 index 1bb424a..0000000 --- a/cmd/tailscale/backend.go +++ /dev/null @@ -1,449 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "errors" - "fmt" - "log" - "net/http" - "net/netip" - "path/filepath" - "reflect" - "strings" - "time" - - "github.com/tailscale/tailscale-android/cmd/jni" - "github.com/tailscale/wireguard-go/tun" - - "golang.org/x/sys/unix" - "tailscale.com/ipn" - "tailscale.com/ipn/ipnlocal" - "tailscale.com/logpolicy" - "tailscale.com/logtail" - "tailscale.com/logtail/filch" - "tailscale.com/net/dns" - "tailscale.com/net/netmon" - "tailscale.com/net/tsdial" - "tailscale.com/tsd" - "tailscale.com/types/logger" - "tailscale.com/types/logid" - "tailscale.com/util/clientmetric" - "tailscale.com/util/dnsname" - "tailscale.com/wgengine" - "tailscale.com/wgengine/netstack" - "tailscale.com/wgengine/router" -) - -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 - -const ( - logPrefKey = "privatelogid" - loginMethodPrefKey = "loginmethod" - customLoginServerPrefKey = "customloginserver" - exitNodePrefKey = "exitnode" - exitAllowLANPrefKey = "exitallowlan" -) - -const ( - loginMethodGoogle = "google" - loginMethodWeb = "web" -) - -// 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"), -} - -// 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 -} - -func (b *backend) Start(notify func(n ipn.Notify)) error { - b.backend.SetNotifyCallback(notify) - return b.backend.Start(ipn.Options{}) -} - -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() -} - -// 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, - HTTPC: &http.Client{Transport: transport}, - CompressLogs: true, - } - 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) 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 -} diff --git a/cmd/tailscale/callbacks.go b/cmd/tailscale/callbacks.go deleted file mode 100644 index 6f5d1d5..0000000 --- a/cmd/tailscale/callbacks.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package main - -// JNI implementations of Java native callback methods. - -import ( - "unsafe" - - "github.com/tailscale/tailscale-android/cmd/jni" -) - -// #include -import "C" - -var ( - // onVPNPrepared is notified when VpnService.prepare succeeds. - onVPNPrepared = make(chan struct{}, 1) - // onVPNClosed is notified when VpnService.prepare fails, or when - // the a running VPN connection is closed. - onVPNClosed = make(chan struct{}, 1) - // onVPNRevoked is notified whenever the VPN service is revoked. - onVPNRevoked = make(chan struct{}, 1) - - // onVPNRequested receives global IPNService references when - // a VPN connection is requested. - onVPNRequested = make(chan jni.Object) - // onDisconnect receives global IPNService references when - // disconnecting. - onDisconnect = make(chan jni.Object) - - // onGoogleToken receives google ID tokens. - onGoogleToken = make(chan string) - - // onFileShare receives file sharing intents. - onFileShare = make(chan []File, 1) - - // onWriteStorageGranted is notified when we are granted WRITE_STORAGE_PERMISSION. - onWriteStorageGranted = make(chan struct{}, 1) - - // onDNSConfigChanged is notified when the network changes and the DNS config needs to be updated. - onDNSConfigChanged = make(chan struct{}, 1) -) - -const ( - // Request codes for Android callbacks. - // requestSignin is for Google Sign-In. - requestSignin C.jint = 1000 + iota - // requestPrepareVPN is for when Android's VpnService.prepare - // completes. - requestPrepareVPN -) - -// resultOK is Android's Activity.RESULT_OK. -const resultOK = -1 - -//export Java_com_tailscale_ipn_App_onVPNPrepared -func Java_com_tailscale_ipn_App_onVPNPrepared(env *C.JNIEnv, class C.jclass) { - notifyVPNPrepared() -} - -//export Java_com_tailscale_ipn_App_onWriteStorageGranted -func Java_com_tailscale_ipn_App_onWriteStorageGranted(env *C.JNIEnv, class C.jclass) { - select { - case onWriteStorageGranted <- struct{}{}: - default: - } -} - -func notifyVPNPrepared() { - select { - case onVPNPrepared <- struct{}{}: - default: - } -} - -func notifyVPNRevoked() { - select { - case onVPNRevoked <- struct{}{}: - default: - } -} - -func notifyVPNClosed() { - select { - case onVPNClosed <- struct{}{}: - default: - } -} - -//export Java_com_tailscale_ipn_IPNService_requestVPN -func Java_com_tailscale_ipn_IPNService_requestVPN(env *C.JNIEnv, this C.jobject) { - jenv := (*jni.Env)(unsafe.Pointer(env)) - onVPNRequested <- jni.NewGlobalRef(jenv, jni.Object(this)) -} - -//export Java_com_tailscale_ipn_IPNService_connect -func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) { - requestBackend(ConnectEvent{Enable: true}) -} - -//export Java_com_tailscale_ipn_IPNService_disconnect -func Java_com_tailscale_ipn_IPNService_disconnect(env *C.JNIEnv, this C.jobject) { - jenv := (*jni.Env)(unsafe.Pointer(env)) - onDisconnect <- jni.NewGlobalRef(jenv, jni.Object(this)) -} - -//export Java_com_tailscale_ipn_StartVPNWorker_connect -func Java_com_tailscale_ipn_StartVPNWorker_connect(env *C.JNIEnv, this C.jobject) { - requestBackend(ConnectEvent{Enable: true}) -} - -//export Java_com_tailscale_ipn_StopVPNWorker_disconnect -func Java_com_tailscale_ipn_StopVPNWorker_disconnect(env *C.JNIEnv, this C.jobject) { - requestBackend(ConnectEvent{Enable: false}) -} - -//export Java_com_tailscale_ipn_Peer_onActivityResult0 -func Java_com_tailscale_ipn_Peer_onActivityResult0(env *C.JNIEnv, cls C.jclass, act C.jobject, reqCode, resCode C.jint) { - switch reqCode { - case requestSignin: - if resCode != resultOK { - onGoogleToken <- "" - break - } - jenv := (*jni.Env)(unsafe.Pointer(env)) - m := jni.GetStaticMethodID(jenv, googleClass, - "getIdTokenForActivity", "(Landroid/app/Activity;)Ljava/lang/String;") - idToken, err := jni.CallStaticObjectMethod(jenv, googleClass, m, jni.Value(act)) - if err != nil { - fatalErr(err) - break - } - tok := jni.GoString(jenv, jni.String(idToken)) - onGoogleToken <- tok - case requestPrepareVPN: - if resCode == resultOK { - notifyVPNPrepared() - } else { - notifyVPNClosed() - notifyVPNRevoked() - } - } -} - -//export Java_com_tailscale_ipn_App_onShareIntent -func Java_com_tailscale_ipn_App_onShareIntent(env *C.JNIEnv, cls C.jclass, nfiles C.jint, jtypes C.jintArray, jmimes C.jobjectArray, jitems C.jobjectArray, jnames C.jobjectArray, jsizes C.jlongArray) { - const ( - typeNone = 0 - typeInline = 1 - typeURI = 2 - ) - jenv := (*jni.Env)(unsafe.Pointer(env)) - types := jni.GetIntArrayElements(jenv, jni.IntArray(jtypes)) - mimes := jni.GetStringArrayElements(jenv, jni.ObjectArray(jmimes)) - items := jni.GetStringArrayElements(jenv, jni.ObjectArray(jitems)) - names := jni.GetStringArrayElements(jenv, jni.ObjectArray(jnames)) - sizes := jni.GetLongArrayElements(jenv, jni.LongArray(jsizes)) - var files []File - for i := 0; i < int(nfiles); i++ { - f := File{ - Type: FileType(types[i]), - MIMEType: mimes[i], - Name: names[i], - } - if f.Name == "" { - f.Name = "file.bin" - } - switch f.Type { - case FileTypeText: - f.Text = items[i] - f.Size = int64(len(f.Text)) - case FileTypeURI: - f.URI = items[i] - f.Size = sizes[i] - default: - panic("unknown file type") - } - files = append(files, f) - } - select { - case <-onFileShare: - default: - } - onFileShare <- files -} - -//export Java_com_tailscale_ipn_App_onDnsConfigChanged -func Java_com_tailscale_ipn_App_onDnsConfigChanged(env *C.JNIEnv, cls C.jclass) { - select { - case onDNSConfigChanged <- struct{}{}: - default: - } -} diff --git a/cmd/tailscale/google.png b/cmd/tailscale/google.png deleted file mode 100644 index a9de963..0000000 Binary files a/cmd/tailscale/google.png and /dev/null differ diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go deleted file mode 100644 index 5973901..0000000 --- a/cmd/tailscale/main.go +++ /dev/null @@ -1,1702 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "cmp" - "context" - "crypto/rand" - "crypto/sha1" - "encoding/hex" - "errors" - "fmt" - "io" - "log" - "mime" - "net" - "net/http" - "net/netip" - "net/url" - "os" - "path/filepath" - "sort" - "strings" - "sync/atomic" - "time" - "unsafe" - - "gioui.org/app" - "gioui.org/io/key" - "gioui.org/io/system" - "gioui.org/layout" - "gioui.org/op" - "golang.org/x/exp/maps" - "inet.af/netaddr" - - "github.com/tailscale/tailscale-android/cmd/jni" - "github.com/tailscale/tailscale-android/cmd/localapiservice" - - "tailscale.com/client/tailscale/apitype" - "tailscale.com/hostinfo" - "tailscale.com/ipn" - "tailscale.com/ipn/ipnlocal" - "tailscale.com/ipn/localapi" - "tailscale.com/net/dns" - "tailscale.com/net/interfaces" - "tailscale.com/net/netns" - "tailscale.com/paths" - "tailscale.com/tailcfg" - "tailscale.com/types/logid" - "tailscale.com/types/netmap" - "tailscale.com/util/syspolicy" - "tailscale.com/wgengine/router" -) - -type App struct { - jvm *jni.JVM - // appCtx is a global reference to the com.tailscale.ipn.App instance. - appCtx jni.Object - - store *stateStore - logIDPublicAtomic atomic.Pointer[logid.PublicID] - - localAPI *localapiservice.LocalAPIService - backend *ipnlocal.LocalBackend - - // netStates receives the most recent network state. - netStates chan BackendState - // prefs receives new preferences from the backend. - prefs chan *ipn.Prefs - // browseURLs receives URLs when the backend wants to browse. - browseURLs chan string - // targetsLoaded receives lists of file targets. - targetsLoaded chan FileTargets - // invalidates receives whenever the window should be refreshed. - invalidates chan struct{} - // bugReport receives the bug report from the backend's localapi call - bugReport chan string -} - -var ( - // googleClass is a global reference to the com.tailscale.ipn.Google class. - googleClass jni.Class -) - -type FileTargets struct { - Targets []*apitype.FileTarget - Err error -} - -type File struct { - Type FileType - Name string - Size int64 - MIMEType string - // URI of the file, valid if Type is FileTypeURI. - URI string - // Text is the content of the file, if Type is FileTypeText. - Text string -} - -// FileSendInfo describes the state of an ongoing file send operation. -type FileSendInfo struct { - State FileSendState - // Progress tracks the progress of the transfer from 0.0 to 1.0. Valid - // only when State is FileSendStarted. - Progress float64 -} - -type clientState struct { - browseURL string - backend BackendState - // query is the search query, in lowercase. - query string - - Peers []UIPeer -} - -type FileType uint8 - -// FileType constants are known to IPNActivity.java. -const ( - FileTypeText FileType = 1 - FileTypeURI FileType = 2 -) - -type ExitStatus uint8 - -const ( - // No exit node selected. - ExitNone ExitStatus = iota - // Exit node selected and exists, but is offline or missing. - ExitOffline - // Exit node selected and online. - ExitOnline -) - -type FileSendState uint8 - -const ( - FileSendNotStarted FileSendState = iota - FileSendConnecting - FileSendTransferring - FileSendComplete - FileSendFailed -) - -type Peer struct { - Label string - Online bool - ID tailcfg.StableNodeID - Location *tailcfg.Location - PreferredExitNode bool -} - -type BackendState struct { - Prefs *ipn.Prefs - State ipn.State - NetworkMap *netmap.NetworkMap - LostInternet bool - // Exits are the peers that can act as exit node. - Exits []Peer - // ExitState describes the state of our exit node. - ExitStatus ExitStatus - // Exit is our current exit node, if any. - Exit Peer -} - -// UIEvent is an event flowing from the UI to the backend. -type UIEvent interface{} - -type RouteAllEvent struct { - ID tailcfg.StableNodeID -} - -type ConnectEvent struct { - Enable bool - SettingsRestoreEvent -} - -type CopyEvent struct { - Text string -} - -type SearchEvent struct { - Query string -} - -type OAuth2Event struct { - Token *tailcfg.Oauth2Token -} - -type FileSendEvent struct { - Target *apitype.FileTarget - Context context.Context - Updates func(FileSendInfo) -} - -type SetLoginServerEvent struct { - URL string -} - -type WebAuthEvent struct { - SettingsRestoreEvent -} - -// UIEvent types. -type ( - ReauthEvent struct{} - BugEvent struct{} - GoogleAuthEvent struct{} - LogoutEvent struct{} - OSSLicensesEvent struct{} - BeExitNodeEvent bool - ExitAllowLANEvent bool -) - -// RestoreEvent represents an event that might restore user settings persisted across sessions. -type RestoreEvent interface { - SetURL(url string) - GetURL() string - SetExitNodeID(exitNodeID tailcfg.StableNodeID) - GetExitNodeID() tailcfg.StableNodeID - SetExitAllowLAN(allowLAN bool) - GetExitAllowLAN() bool -} - -type SettingsRestoreEvent struct { - // Custom server login URL - URL string - ExitNodeID tailcfg.StableNodeID - ExitAllowLAN bool -} - -func (e *SettingsRestoreEvent) SetURL(url string) { - e.URL = url -} -func (e *SettingsRestoreEvent) GetURL() string { - return e.URL -} -func (e *SettingsRestoreEvent) SetExitNodeID(exitNodeID tailcfg.StableNodeID) { - e.ExitNodeID = exitNodeID -} -func (e *SettingsRestoreEvent) GetExitNodeID() tailcfg.StableNodeID { - return e.ExitNodeID -} -func (e *SettingsRestoreEvent) SetExitAllowLAN(allowLAN bool) { - e.ExitAllowLAN = allowLAN -} -func (e *SettingsRestoreEvent) GetExitAllowLAN() bool { - return e.ExitAllowLAN -} - -// serverOAuthID is the OAuth ID of the tailscale-android server, used -// by GoogleSignInOptions.Builder.requestIdToken. -const serverOAuthID = "744055068597-hv4opg0h7vskq1hv37nq3u26t8c15qk0.apps.googleusercontent.com" - -// releaseCertFingerprint is the SHA-1 fingerprint of the Google Play Store signing key. -// It is used to check whether the app is signed for release. -const releaseCertFingerprint = "86:9D:11:8B:63:1E:F8:35:C6:D9:C2:66:53:BC:28:22:2F:B8:C1:AE" - -// backendEvents receives events from the UI (Activity, Tile etc.) to the backend. -var backendEvents = make(chan UIEvent) - -func main() { - a := &App{ - jvm: (*jni.JVM)(unsafe.Pointer(app.JavaVM())), - appCtx: jni.Object(app.AppContext()), - netStates: make(chan BackendState, 1), - browseURLs: make(chan string, 1), - prefs: make(chan *ipn.Prefs, 1), - targetsLoaded: make(chan FileTargets, 1), - invalidates: make(chan struct{}, 1), - bugReport: make(chan string, 1), - } - - err := a.loadJNIGlobalClassRefs() - if err != nil { - fatalErr(err) - } - - a.store = newStateStore(a.jvm, a.appCtx) - interfaces.RegisterInterfaceGetter(a.getInterfaces) - syspolicy.RegisterHandler(androidHandler{a: a}) - go func() { - ctx := context.Background() - if err := a.runBackend(ctx); err != nil { - fatalErr(err) - } - }() - go func() { - if err := a.runUI(); err != nil { - fatalErr(err) - } - }() - app.Main() -} - -// Loads the global JNI class references. Failures here are fatal if the -// class ref is required for the app to function. -func (a *App) loadJNIGlobalClassRefs() error { - return jni.Do(a.jvm, func(env *jni.Env) error { - loader := jni.ClassLoaderFor(env, a.appCtx) - cl, err := jni.LoadClass(env, loader, "com.tailscale.ipn.Google") - if err != nil { - // Ignore load errors; the Google class is not included in F-Droid builds. - return nil - } - googleClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl))) - return nil - }) -} - -func (a *App) runBackend(ctx context.Context) error { - appDir, err := app.DataDir() - if err != nil { - fatalErr(err) - } - paths.AppSharedDir.Store(appDir) - hostinfo.SetOSVersion(a.osVersion()) - if !googleSignInEnabled() { - hostinfo.SetPackage("nogoogle") - } - deviceModel := a.modelName() - if a.isChromeOS() { - deviceModel = "ChromeOS: " + deviceModel - } - hostinfo.SetDeviceModel(deviceModel) - - 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) - - localapiservice.ConfigureShim(a.jvm, a.appCtx, a.localAPI, b.backend) - - // Contrary to the documentation for VpnService.Builder.addDnsServer, - // ChromeOS doesn't fall back to the underlying network nameservers if - // we don't provide any. - b.avoidEmptyDNS = a.isChromeOS() - - var timer *time.Timer - var alarmChan <-chan time.Time - alarm := func(t *time.Timer) { - if timer != nil { - timer.Stop() - } - timer = t - if timer != nil { - alarmChan = timer.C - } - } - notifications := make(chan ipn.Notify, 1) - startErr := make(chan error) - // Start from a goroutine to avoid deadlock when Start - // calls the callback. - go func() { - startErr <- b.Start(func(n ipn.Notify) { - notifications <- n - }) - }() - var ( - cfg configPair - state BackendState - service jni.Object // of IPNService - signingIn bool - ) - var ( - waitingFilesDone = make(chan struct{}) - waitingFiles bool - processingFiles bool - ) - processFiles := func() { - if !waitingFiles || processingFiles { - return - } - processingFiles = true - waitingFiles = false - go func() { - if err := a.processWaitingFiles(b.backend); err != nil { - log.Printf("processWaitingFiles: %v", err) - } - waitingFilesDone <- struct{}{} - }() - } - for { - select { - case err := <-startErr: - if err != nil { - return err - } - case <-waitingFilesDone: - processingFiles = false - processFiles() - case s := <-configs: - cfg = s - if b == nil || service == 0 || cfg.rcfg == nil { - configErrs <- nil - break - } - configErrs <- b.updateTUN(service, cfg.rcfg, cfg.dcfg) - case n := <-notifications: - exitWasOnline := state.ExitStatus == ExitOnline - if p := n.Prefs; p != nil && n.Prefs.Valid() { - state.Prefs = p.AsStruct() - state.updateExitNodes() - a.setPrefs(state.Prefs) - } - first := state.Prefs == nil - if first { - state.Prefs = ipn.NewPrefs() - state.Prefs.Hostname = a.hostname() - go b.backend.SetPrefs(state.Prefs) - a.setPrefs(state.Prefs) - } - if s := n.State; s != nil { - oldState := state.State - state.State = *s - if service != 0 { - if cfg.rcfg != nil && state.State >= ipn.Starting { - if err := b.updateTUN(service, cfg.rcfg, cfg.dcfg); err != nil { - if errors.Is(err, errMultipleUsers) { - a.pushNotify(service, "Multiple Users", multipleUsersText) - } - log.Printf("VPN update failed: %v", err) - notifyVPNClosed() - } - } else { - b.CloseTUNs() - } - } - // Stop VPN if we logged out. - if oldState > ipn.Stopped && state.State <= ipn.Stopped { - if err := a.callVoidMethod(a.appCtx, "stopVPN", "()V"); err != nil { - fatalErr(err) - } - } - a.notify(state) - if service != 0 { - a.updateNotification(service, state.State, state.ExitStatus, state.Exit) - } - } - if u := n.BrowseToURL; u != nil { - signingIn = false - a.setURL(*u) - } - if m := n.NetMap; m != nil { - state.NetworkMap = m - state.updateExitNodes() - a.notify(state) - if service != 0 { - alarm(a.notifyExpiry(service, m.Expiry)) - } - } - // Notify if a previously online exit is not longer online (or missing). - if service != 0 && exitWasOnline && state.ExitStatus == ExitOffline { - a.pushNotify(service, "Connection Lost", "Your exit node is offline. Disable your exit node or contact your network admin for help.") - } - targets, err := b.backend.FileTargets() - if err != nil { - // Construct a user-visible error message. - if b.backend.State() != ipn.Running { - err = fmt.Errorf("Not connected to tailscale") - } else { - err = fmt.Errorf("Failed to load device list") - } - } - a.targetsLoaded <- FileTargets{targets, err} - waitingFiles = n.FilesWaiting != nil - processFiles() - case <-onWriteStorageGranted: - processFiles() - case <-alarmChan: - if m := state.NetworkMap; m != nil && service != 0 { - alarm(a.notifyExpiry(service, m.Expiry)) - } - case e := <-backendEvents: - switch e := e.(type) { - case BugEvent: - backendLogIDStr := a.logIDPublicAtomic.Load().String() - fallbackLog := fmt.Sprintf("BUG-%v-%v-%v", backendLogIDStr, time.Now().UTC().Format("20060102150405Z"), randHex(8)) - a.localAPI.GetBugReportID(ctx, a.bugReport, fallbackLog) - case OAuth2Event: - go b.backend.Login(e.Token) - case BeExitNodeEvent: - state.Prefs.SetAdvertiseExitNode(bool(e)) - go b.backend.SetPrefs(state.Prefs) - case ExitAllowLANEvent: - state.Prefs.ExitNodeAllowLANAccess = bool(e) - a.store.WriteBool(exitAllowLANPrefKey, true) - go b.backend.SetPrefs(state.Prefs) - case WebAuthEvent: - if !signingIn { - setCustomServer, setExitNode, setAllowLANAccess := a.restoreSettings(&e, state, service) - signingIn = true - go func() { - if setCustomServer || setExitNode || setAllowLANAccess { - b.backend.SetPrefs(state.Prefs) - } - if setCustomServer { - // Need to restart to force the login URL to be regenerated - // with the new control URL. Start from a goroutine to avoid - // deadlock. - err := b.backend.Start(ipn.Options{}) - if err != nil { - fatalErr(err) - } - } - b.backend.StartLoginInteractive() - }() - } - case SetLoginServerEvent: - state.Prefs.ControlURL = e.URL - a.store.WriteString(customLoginServerPrefKey, e.URL) - b.backend.SetPrefs(state.Prefs) - // Need to restart to force the login URL to be regenerated - // with the new control URL. Start from a goroutine to avoid - // deadlock. - go func() { - err := b.backend.Start(ipn.Options{}) - if err != nil { - fatalErr(err) - } - }() - case LogoutEvent: - go a.localAPI.Logout(ctx, a.backend) - case ConnectEvent: - first := state.Prefs == nil - // A ConnectEvent might be sent before the first notification - // arrives, such as in the case of Always-on VPN. - if first { - state.Prefs = ipn.NewPrefs() - } - state.Prefs.WantRunning = e.Enable - setCustomServer, _, _ := a.restoreSettings(&e, state, service) - go func() { - b.backend.SetPrefs(state.Prefs) - if setCustomServer { - // Need to restart to force the login URL to be regenerated - // with the new control URL. Start from a goroutine to avoid - // deadlock. - err := b.backend.Start(ipn.Options{}) - if err != nil { - fatalErr(err) - } - } - }() - case RouteAllEvent: - state.Prefs.ExitNodeID = e.ID - go b.backend.SetPrefs(state.Prefs) - a.store.WriteString(exitNodePrefKey, string(e.ID)) - state.updateExitNodes() - a.notify(state) - if service != 0 { - a.updateNotification(service, state.State, state.ExitStatus, state.Exit) - } - } - case s := <-onVPNRequested: - jni.Do(a.jvm, func(env *jni.Env) error { - if jni.IsSameObject(env, s, service) { - // We already have a reference. - jni.DeleteGlobalRef(env, s) - return nil - } - if service != 0 { - jni.DeleteGlobalRef(env, service) - } - netns.SetAndroidProtectFunc(func(fd int) error { - return jni.Do(a.jvm, func(env *jni.Env) error { - // Call https://developer.android.com/reference/android/net/VpnService#protect(int) - // to mark fd as a socket that should bypass the VPN and use the underlying network. - cls := jni.GetObjectClass(env, s) - m := jni.GetMethodID(env, cls, "protect", "(I)Z") - ok, err := jni.CallBooleanMethod(env, s, m, jni.Value(fd)) - // TODO(bradfitz): return an error back up to netns if this fails, once - // we've had some experience with this and analyzed the logs over a wide - // range of Android phones. For now we're being paranoid and conservative - // and do the JNI call to protect best effort, only logging if it fails. - // The risk of returning an error is that it breaks users on some Android - // versions even when they're not using exit nodes. I'd rather the - // relatively few number of exit node users file bug reports if Tailscale - // doesn't work and then we can look for this log print. - if err != nil || !ok { - log.Printf("[unexpected] VpnService.protect(%d) = %v, %v", fd, ok, err) - } - return nil // even on error. see big TODO above. - }) - }) - log.Printf("onVPNRequested: rebind required") - // TODO(catzkorn): When we start the android application - // we bind sockets before we have access to the VpnService.protect() - // function which is needed to avoid routing loops. When we activate - // the service we get access to the protect, but do not retrospectively - // protect the sockets already opened, which breaks connectivity. - // As a temporary fix, we rebind and protect the magicsock.Conn on connect - // which restores connectivity. - // See https://github.com/tailscale/corp/issues/13814 - b.backend.DebugRebind() - - service = s - return nil - }) - a.updateNotification(service, state.State, state.ExitStatus, state.Exit) - if m := state.NetworkMap; m != nil { - alarm(a.notifyExpiry(service, m.Expiry)) - } - if cfg.rcfg != nil && state.State >= ipn.Starting { - if err := b.updateTUN(service, cfg.rcfg, cfg.dcfg); err != nil { - log.Printf("VPN update failed: %v", err) - notifyVPNClosed() - } - } - case s := <-onDisconnect: - b.CloseTUNs() - jni.Do(a.jvm, func(env *jni.Env) error { - defer jni.DeleteGlobalRef(env, s) - if jni.IsSameObject(env, service, s) { - netns.SetAndroidProtectFunc(nil) - jni.DeleteGlobalRef(env, service) - service = 0 - } - return nil - }) - if state.State >= ipn.Starting { - notifyVPNClosed() - } - case <-onDNSConfigChanged: - if b != nil { - go b.NetworkChanged() - } - a.notify(state) - } - } -} - -func (a *App) restoreSettings(e RestoreEvent, state BackendState, service jni.Object) (bool, bool, bool) { - var setCustomServer bool - var setExitNode bool - var setAllowLANAccess bool - if URL := e.GetURL(); URL != "" { - state.Prefs.ControlURL = URL - setCustomServer = true - } - if nodeID := e.GetExitNodeID(); nodeID != "" { - state.Prefs.ExitNodeID = nodeID - state.updateExitNodes() - a.notify(state) - if service != 0 { - a.updateNotification(service, state.State, state.ExitStatus, state.Exit) - } - setExitNode = true - } - if e.GetExitAllowLAN() { - state.Prefs.ExitNodeAllowLANAccess = true - } - return setCustomServer, setExitNode, setAllowLANAccess -} - -func (a *App) processWaitingFiles(b *ipnlocal.LocalBackend) error { - files, err := b.WaitingFiles() - if err != nil { - return err - } - var aerr error - for _, f := range files { - if err := a.downloadFile(b, f); err != nil && aerr == nil { - aerr = err - } - } - return aerr -} - -func (a *App) downloadFile(b *ipnlocal.LocalBackend, f apitype.WaitingFile) (cerr error) { - in, _, err := b.OpenFile(f.Name) - if err != nil { - return err - } - defer in.Close() - ext := filepath.Ext(f.Name) - mimeType := mime.TypeByExtension(ext) - var mediaURI string - err = jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - insertMedia := jni.GetMethodID(env, cls, "insertMedia", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;") - jname := jni.JavaString(env, f.Name) - jmime := jni.JavaString(env, mimeType) - uri, err := jni.CallObjectMethod(env, a.appCtx, insertMedia, jni.Value(jname), jni.Value(jmime)) - if err != nil { - return err - } - mediaURI = jni.GoString(env, jni.String(uri)) - return nil - }) - if err != nil { - return fmt.Errorf("insertMedia: %w", err) - } - deleteURI := func(uri string) error { - return jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - m := jni.GetMethodID(env, cls, "deleteUri", "(Ljava/lang/String;)V") - juri := jni.JavaString(env, uri) - return jni.CallVoidMethod(env, a.appCtx, m, jni.Value(juri)) - }) - } - out, err := a.openURI(mediaURI, "w") - if err != nil { - deleteURI(mediaURI) - return fmt.Errorf("openUri: %w", err) - } - if _, err := io.Copy(out, in); err != nil { - deleteURI(mediaURI) - return fmt.Errorf("copy: %w", err) - } - if err := out.Close(); err != nil { - deleteURI(mediaURI) - return fmt.Errorf("close: %w", err) - } - if err := a.notifyFile(mediaURI, f.Name); err != nil { - fatalErr(err) - } - return b.DeleteFile(f.Name) -} - -// openURI calls a.appCtx.getContentResolver().openFileDescriptor on uri and -// mode and returns the detached file descriptor. -func (a *App) openURI(uri, mode string) (*os.File, error) { - var f *os.File - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - openURI := jni.GetMethodID(env, cls, "openUri", "(Ljava/lang/String;Ljava/lang/String;)I") - juri := jni.JavaString(env, uri) - jmode := jni.JavaString(env, mode) - fd, err := jni.CallIntMethod(env, a.appCtx, openURI, jni.Value(juri), jni.Value(jmode)) - if err != nil { - return err - } - f = os.NewFile(uintptr(fd), "media-store") - return nil - }) - return f, err -} - -func (a *App) isChromeOS() bool { - var chromeOS bool - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - m := jni.GetMethodID(env, cls, "isChromeOS", "()Z") - b, err := jni.CallBooleanMethod(env, a.appCtx, m) - chromeOS = b - return err - }) - if err != nil { - panic(err) - } - return chromeOS -} - -func (s *BackendState) updateExitNodes() { - s.ExitStatus = ExitNone - var exitID tailcfg.StableNodeID - if p := s.Prefs; p != nil { - exitID = p.ExitNodeID - if exitID != "" { - s.ExitStatus = ExitOffline - } - } - hasMyExit := exitID == "" - s.Exits = nil - var peers []tailcfg.NodeView - if s.NetworkMap != nil { - peers = s.NetworkMap.Peers - } - for _, p := range peers { - canRoute := false - for i := range p.AllowedIPs().Len() { - r := p.AllowedIPs().At(i) - if r == netip.MustParsePrefix("0.0.0.0/0") || r == netip.MustParsePrefix("::/0") { - canRoute = true - break - } - } - myExit := p.StableID() == exitID - hasMyExit = hasMyExit || myExit - exit := Peer{ - Label: p.DisplayName(true), - Online: canRoute, - ID: p.StableID(), - Location: p.Hostinfo().Location(), - } - - if exit.Location != nil { - // We want to shorten what the users sees here, - // so override the display name with the computed - // name. - exit.Label = p.ComputedName() - } - - if myExit { - s.Exit = exit - if canRoute { - s.ExitStatus = ExitOnline - } - } - if canRoute || myExit { - s.Exits = append(s.Exits, exit) - } - } - - locationBasedExitPeersMap := make(map[string]Peer) - var nonLocationBasedExitPeers []Peer - var allLocationBasedExitPeers []Peer - for _, peer := range s.Exits { - if peer.Location != nil { - countryCityLocation, ok := locationBasedExitPeersMap[fmt.Sprintf("%s (%s)", peer.Location.Country, peer.Location.City)] - if !ok { - // If we have not seen the country/city combination, add it to the - // map. - locationBasedExitPeersMap[fmt.Sprintf("%s (%s)", peer.Location.Country, peer.Location.City)] = peer - continue - } - - if countryCityLocation.Location.Priority < peer.Location.Priority { - // If the priority for the location based exit node is higher than - // the current option, replace it. - locationBasedExitPeersMap[fmt.Sprintf("%s (%s)", peer.Location.Country, peer.Location.City)] = peer - } - - allLocationBasedExitPeers = append(allLocationBasedExitPeers, peer) - continue - } - nonLocationBasedExitPeers = append(nonLocationBasedExitPeers, peer) - } - - // We want to order the exit nodes to be display to the user in - // the order of non location based exit nodes, the best exit - // node per location, and then all of the location based exit nodes. - - // Non location based exit nodes. - s.Exits = nonLocationBasedExitPeers - sort.Slice(s.Exits, func(i, j int) bool { - return s.Exits[i].Label < s.Exits[j].Label - }) - - // Best location based exit nodes - locationBasedExitPeersMapValues := maps.Values(locationBasedExitPeersMap) - if len(locationBasedExitPeersMapValues) > 0 { - var preferredLocationBasedExitPeers []Peer - for _, peer := range locationBasedExitPeersMapValues { - peerCopy := peer - peerCopy.PreferredExitNode = true - peerCopy.Label = fmt.Sprintf("%s - %s (%s)", peerCopy.Location.Country, peerCopy.Location.City, peerCopy.Label) - - preferredLocationBasedExitPeers = append(preferredLocationBasedExitPeers, peerCopy) - } - - sort.Slice(preferredLocationBasedExitPeers, func(i, j int) bool { - // Sort the order by country, and cities. - res := cmp.Compare(preferredLocationBasedExitPeers[i].Location.Country, preferredLocationBasedExitPeers[j].Location.Country) - - switch res { - case -1: - return true - case 1: - return false - default: - // If the two peers have the same country, sort by city. - return preferredLocationBasedExitPeers[i].Location.City < preferredLocationBasedExitPeers[j].Location.City - } - }) - s.Exits = append(s.Exits, preferredLocationBasedExitPeers...) - } - - if len(allLocationBasedExitPeers) > 0 { - // All location based exit nodes at the end. - sort.Slice(allLocationBasedExitPeers, func(i, j int) bool { - // Sort the order by label - return allLocationBasedExitPeers[i].Label < allLocationBasedExitPeers[j].Label - }) - s.Exits = append(s.Exits, allLocationBasedExitPeers...) - } - - if !hasMyExit { - // Insert node missing from netmap. - s.Exit = Peer{Label: "Unknown device", ID: exitID} - s.Exits = append([]Peer{s.Exit}, s.Exits...) - } -} - -// hostname builds a hostname from android.os.Build fields, in place of a -// useless os.Hostname(). -func (a *App) hostname() string { - var hostname string - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - getHostname := jni.GetMethodID(env, cls, "getHostname", "()Ljava/lang/String;") - n, err := jni.CallObjectMethod(env, a.appCtx, getHostname) - hostname = jni.GoString(env, jni.String(n)) - return err - }) - if err != nil { - panic(err) - } - return hostname -} - -// osVersion returns android.os.Build.VERSION.RELEASE. " [nogoogle]" is appended -// if Google Play services are not compiled in. -func (a *App) osVersion() string { - var version string - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - m := jni.GetMethodID(env, cls, "getOSVersion", "()Ljava/lang/String;") - n, err := jni.CallObjectMethod(env, a.appCtx, m) - version = jni.GoString(env, jni.String(n)) - return err - }) - if err != nil { - panic(err) - } - return version -} - -// modelName return the MANUFACTURER + MODEL from -// android.os.Build. -func (a *App) modelName() string { - var model string - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - m := jni.GetMethodID(env, cls, "getModelName", "()Ljava/lang/String;") - n, err := jni.CallObjectMethod(env, a.appCtx, m) - model = jni.GoString(env, jni.String(n)) - return err - }) - if err != nil { - panic(err) - } - return model -} - -func googleSignInEnabled() bool { - return googleClass != 0 -} - -// updateNotification updates the foreground persistent status notification. -func (a *App) updateNotification(service jni.Object, state ipn.State, exitStatus ExitStatus, exit Peer) error { - var msg, title string - switch state { - case ipn.Starting: - title, msg = "Connecting...", "" - case ipn.Running: - title = "Connected" - switch exitStatus { - case ExitOnline: - msg = fmt.Sprintf("Exit node: %s", exit.Label) - default: - msg = "" - } - default: - return nil - } - return jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, service) - update := jni.GetMethodID(env, cls, "updateStatusNotification", "(Ljava/lang/String;Ljava/lang/String;)V") - jtitle := jni.JavaString(env, title) - jmessage := jni.JavaString(env, msg) - return jni.CallVoidMethod(env, service, update, jni.Value(jtitle), jni.Value(jmessage)) - }) -} - -// notifyExpiry notifies the user of imminent session expiry and -// returns a new timer that triggers when the user should be notified -// again. -func (a *App) notifyExpiry(service jni.Object, expiry time.Time) *time.Timer { - if expiry.IsZero() { - return nil - } - d := time.Until(expiry) - var title string - const msg = "Reauthenticate to maintain the connection to your network." - var t *time.Timer - const ( - aday = 24 * time.Hour - soon = 5 * time.Minute - ) - switch { - case d <= 0: - title = "Your authentication has expired!" - case d <= soon: - title = "Your authentication expires soon!" - t = time.NewTimer(d) - case d <= aday: - title = "Your authentication expires in a day." - t = time.NewTimer(d - soon) - default: - return time.NewTimer(d - aday) - } - if err := a.pushNotify(service, title, msg); err != nil { - fatalErr(err) - } - return t -} - -func (a *App) notifyFile(uri, msg string) error { - return jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - notify := jni.GetMethodID(env, cls, "notifyFile", "(Ljava/lang/String;Ljava/lang/String;)V") - juri := jni.JavaString(env, uri) - jmsg := jni.JavaString(env, msg) - return jni.CallVoidMethod(env, a.appCtx, notify, jni.Value(juri), jni.Value(jmsg)) - }) -} - -func (a *App) pushNotify(service jni.Object, title, msg string) error { - return jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, service) - notify := jni.GetMethodID(env, cls, "notify", "(Ljava/lang/String;Ljava/lang/String;)V") - jtitle := jni.JavaString(env, title) - jmessage := jni.JavaString(env, msg) - return jni.CallVoidMethod(env, service, notify, jni.Value(jtitle), jni.Value(jmessage)) - }) -} - -func (a *App) notify(state BackendState) { - select { - case <-a.netStates: - default: - } - a.netStates <- state - ready := jni.Bool(state.State >= ipn.Stopped) - if err := a.callVoidMethod(a.appCtx, "setTileReady", "(Z)V", jni.Value(ready)); err != nil { - fatalErr(err) - } -} - -func (a *App) setPrefs(prefs *ipn.Prefs) { - wantRunning := jni.Bool(prefs.WantRunning) - if err := a.callVoidMethod(a.appCtx, "setTileStatus", "(Z)V", jni.Value(wantRunning)); err != nil { - fatalErr(err) - } - select { - case <-a.prefs: - default: - } - a.prefs <- prefs -} - -func (a *App) setURL(url string) { - select { - case <-a.browseURLs: - default: - } - a.browseURLs <- url -} - -func (a *App) runUI() error { - w := app.NewWindow() - ui, err := newUI(a.store) - if err != nil { - return err - } - var ops op.Ops - state := new(clientState) - var ( - // activity is the most recent Android Activity reference as reported - // by Gio ViewEvents. - activity jni.Object - // files is list of files from the most recent file sharing intent. - files []File - ) - deleteActivityRef := func() { - if activity == 0 { - return - } - jni.Do(a.jvm, func(env *jni.Env) error { - jni.DeleteGlobalRef(env, activity) - return nil - }) - activity = 0 - } - defer deleteActivityRef() - for { - select { - case <-onVPNClosed: - requestBackend(ConnectEvent{Enable: false}) - case tok := <-onGoogleToken: - ui.signinType = noSignin - if tok != "" { - requestBackend(OAuth2Event{ - Token: &tailcfg.Oauth2Token{ - AccessToken: tok, - TokenType: ipn.GoogleIDTokenType, - }, - }) - } else { - // Warn about possible debug certificate. - if !a.isReleaseSigned() { - ui.ShowMessage("Google Sign-In failed because the app is not signed for Play Store") - w.Invalidate() - } - } - case p := <-a.prefs: - ui.enabled.Value = p.WantRunning - ui.runningExit = p.AdvertisesExitNode() - ui.exitLAN.Value = p.ExitNodeAllowLANAccess - w.Invalidate() - case url := <-a.browseURLs: - ui.signinType = noSignin - if a.isTV() { - ui.ShowQRCode(url) - } else { - state.browseURL = url - } - w.Invalidate() - a.updateState(activity, state) - case newState := <-a.netStates: - oldState := state.backend.State - state.backend = newState - a.updateState(activity, state) - w.Invalidate() - if activity != 0 { - newState := state.backend.State - // Start VPN if we just logged in. - if oldState <= ipn.Stopped && newState > ipn.Stopped { - if err := a.prepareVPN(activity); err != nil { - fatalErr(err) - } - } - } - case <-onVPNPrepared: - if state.backend.State > ipn.Stopped { - // If there isn't a foreground activity start the VPN right away. - if activity == 0 { - if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil { - return err - } - } else { - // Otherwise, check for notification permission and let the result - // of that start the VPN service. - if err := a.callVoidMethod(a.appCtx, "requestNotificationPermission", "(Landroid/app/Activity;)V", jni.Value(activity)); err != nil { - return err - } - if err := a.callVoidMethod(a.appCtx, "requestWriteStoragePermission", "(Landroid/app/Activity;)V", jni.Value(activity)); err != nil { - return err - } - } - } - case <-onVPNRevoked: - ui.ShowMessage("VPN access denied or another VPN service is always-on") - w.Invalidate() - case files = <-onFileShare: - ui.ShowShareDialog() - w.Invalidate() - case t := <-a.targetsLoaded: - ui.FillShareDialog(t.Targets, t.Err) - w.Invalidate() - case <-a.invalidates: - w.Invalidate() - case e := <-w.Events(): - switch e := e.(type) { - case app.ViewEvent: - deleteActivityRef() - view := jni.Object(e.View) - if view == 0 { - break - } - activity = a.contextForView(view) - w.Invalidate() - a.attachPeer(activity) - if state.backend.State > ipn.Stopped { - if err := a.prepareVPN(activity); err != nil { - return err - } - } - case system.DestroyEvent: - return e.Err - case key.Event: - if e.Name == key.NameBack { - if ui.onBack() { - w.Invalidate() - } - } - case system.FrameEvent: - ins := e.Insets - e.Insets = system.Insets{} - gtx := layout.NewContext(&ops, e) - events := ui.layout(gtx, ins, state) - e.Frame(gtx.Ops) - a.processUIEvents(w, events, activity, state, files) - } - } - } -} - -func (a *App) isTV() bool { - var istv bool - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - m := jni.GetMethodID(env, cls, "isTV", "()Z") - b, err := jni.CallBooleanMethod(env, a.appCtx, m) - istv = b - return err - }) - if err != nil { - fatalErr(err) - } - return istv -} - -// isReleaseSigned reports whether the app is signed with a release -// signature. -func (a *App) isReleaseSigned() bool { - var cert []byte - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, a.appCtx) - m := jni.GetMethodID(env, cls, "getPackageCertificate", "()[B") - str, err := jni.CallObjectMethod(env, a.appCtx, m) - if err != nil { - return err - } - cert = jni.GetByteArrayElements(env, jni.ByteArray(str)) - return nil - }) - if err != nil { - fatalErr(err) - } - h := sha1.New() - h.Write(cert) - fingerprint := h.Sum(nil) - hex := fmt.Sprintf("%x", fingerprint) - // Strip colons and convert to lower case to ease comparing. - wantFingerprint := strings.ReplaceAll(strings.ToLower(releaseCertFingerprint), ":", "") - return hex == wantFingerprint -} - -// attachPeer registers an Android Fragment instance for -// handling onActivityResult callbacks. -func (a *App) attachPeer(act jni.Object) { - err := a.callVoidMethod(a.appCtx, "attachPeer", "(Landroid/app/Activity;)V", jni.Value(act)) - if err != nil { - fatalErr(err) - } -} - -func (a *App) updateState(act jni.Object, state *clientState) { - if act != 0 && state.browseURL != "" { - a.browseToURL(act, state.browseURL) - state.browseURL = "" - } - - netmap := state.backend.NetworkMap - var ( - peers []tailcfg.NodeView - myID tailcfg.UserID - ) - if netmap != nil { - peers = netmap.Peers - myID = netmap.User() - } - // Split into sections. - users := make(map[tailcfg.UserID]struct{}) - var uiPeers []UIPeer - for _, p := range peers { - if p.Hostinfo().Valid() && p.Hostinfo().ShareeNode() { - // Don't show nodes that only exist in the netmap because they're - // owned by somebody the user shared a node with. We can't see their - // details (including their name) anyway, so there's nothing - // interesting to render. - continue - } - if q := state.query; q != "" { - // Filter peers according to search query. - host := strings.ToLower(p.Hostinfo().Hostname()) - name := strings.ToLower(p.Name()) - var addr string - if p.Addresses().Len() > 0 { - addr = p.Addresses().At(0).Addr().String() - } - if !strings.Contains(host, q) && !strings.Contains(name, q) && !strings.Contains(addr, q) { - continue - } - } - uid := p.SharerOrUser() - users[uid] = struct{}{} - uiPeers = append(uiPeers, UIPeer{ - Owner: uid, - Peer: p.AsStruct(), - }) - } - // Add section (user) headers. - for u := range users { - name := netmap.UserProfiles[u].DisplayName - name = strings.ToUpper(name) - uiPeers = append(uiPeers, UIPeer{Owner: u, Name: name}) - } - sort.Slice(uiPeers, func(i, j int) bool { - lhs, rhs := uiPeers[i], uiPeers[j] - if lu, ru := lhs.Owner, rhs.Owner; ru != lu { - // Sort own peers first. - if lu == myID { - return true - } - if ru == myID { - return false - } - return lu < ru - } - lp, rp := lhs.Peer, rhs.Peer - // Sort headers first. - if lp == nil { - return true - } - if rp == nil { - return false - } - lName := lp.DisplayName(lp.User == myID) - rName := rp.DisplayName(rp.User == myID) - return lName < rName || lName == rName && lp.ID < rp.ID - }) - state.Peers = uiPeers -} - -func (a *App) prepareVPN(act jni.Object) error { - return a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;I)V", - jni.Value(act), jni.Value(requestPrepareVPN)) -} - -func requestBackend(e UIEvent) { - go func() { - backendEvents <- e - }() -} - -func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, state *clientState, files []File) { - for _, e := range events { - switch e := e.(type) { - case ReauthEvent: - method, _ := a.store.ReadString(loginMethodPrefKey, loginMethodWeb) - switch method { - case loginMethodGoogle: - a.googleSignIn(act) - default: - requestBackend(WebAuthEvent{}) - } - case BugEvent: - // clear the channel in case there's an old bug report hanging out there - select { - case oldReport := <-a.bugReport: - log.Printf("clearing old bug report in channel: %s", oldReport) - default: - break - } - requestBackend(e) - select { - case bug := <-a.bugReport: - w.WriteClipboard(bug) - case <-time.After(2 * time.Second): - // if we don't get a bug through the channel, fall back and create bug report here - backendLogID := a.logIDPublicAtomic.Load() - logMarker := fmt.Sprintf("BUG-%v-%v-%v", backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8)) - log.Printf("bug report fallback because timed out. fallback report: %s", logMarker) - w.WriteClipboard(logMarker) - } - case BeExitNodeEvent: - requestBackend(e) - case ExitAllowLANEvent: - requestBackend(e) - case WebAuthEvent: - a.decorateEventWithStoredSettings(&e, state) - a.store.WriteString(loginMethodPrefKey, loginMethodWeb) - requestBackend(e) - case SetLoginServerEvent: - a.store.WriteString(customLoginServerPrefKey, e.URL) - requestBackend(e) - case LogoutEvent: - a.signOut() - requestBackend(e) - case ConnectEvent: - a.decorateEventWithStoredSettings(&e, state) - requestBackend(e) - case RouteAllEvent: - requestBackend(e) - case CopyEvent: - w.WriteClipboard(e.Text) - case GoogleAuthEvent: - a.store.WriteString(loginMethodPrefKey, loginMethodGoogle) - a.googleSignIn(act) - case SearchEvent: - state.query = strings.ToLower(e.Query) - a.updateState(act, state) - case FileSendEvent: - a.sendFiles(e, files) - case OSSLicensesEvent: - a.setURL("https://tailscale.com/licenses/android") - } - } -} - -func (a *App) decorateEventWithStoredSettings(e RestoreEvent, state *clientState) { - srv, _ := a.store.ReadString(customLoginServerPrefKey, "") - if srv != "" && srv != state.backend.Prefs.ControlURL { - e.SetURL(srv) - } - - exitstr, _ := a.store.ReadString(exitNodePrefKey, "") - exitNodeID := tailcfg.StableNodeID(exitstr) - if exitNodeID != "" && exitNodeID != state.backend.Prefs.ExitNodeID { - e.SetExitNodeID(exitNodeID) - } - - allowlan, _ := a.store.ReadBool(exitAllowLANPrefKey, false) - if allowlan { - e.SetExitAllowLAN(allowlan) - } -} - -func (a *App) sendFiles(e FileSendEvent, files []File) { - go func() { - var totalSize int64 - for _, f := range files { - totalSize += f.Size - } - if totalSize == 0 { - totalSize = 1 - } - var totalSent int64 - progress := func(n int64) { - totalSent += n - e.Updates(FileSendInfo{ - State: FileSendTransferring, - Progress: float64(totalSent) / float64(totalSize), - }) - a.invalidate() - } - defer a.invalidate() - for _, f := range files { - if err := a.sendFile(e.Context, e.Target, f, progress); err != nil { - if errors.Is(err, context.Canceled) { - return - } - e.Updates(FileSendInfo{ - State: FileSendFailed, - }) - return - } - } - e.Updates(FileSendInfo{ - State: FileSendComplete, - }) - }() -} - -func (a *App) invalidate() { - select { - case a.invalidates <- struct{}{}: - default: - } -} - -func (a *App) sendFile(ctx context.Context, target *apitype.FileTarget, f File, progress func(n int64)) error { - var body io.Reader - switch f.Type { - case FileTypeText: - body = strings.NewReader(f.Text) - case FileTypeURI: - f, err := a.openURI(f.URI, "r") - if err != nil { - return err - } - defer f.Close() - body = f - default: - panic("unknown file type") - } - body = &progressReader{r: body, size: f.Size, progress: progress} - dstURL := target.PeerAPIURL + "/v0/put/" + url.PathEscape(f.Name) - req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, body) - if err != nil { - return err - } - req.ContentLength = f.Size - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != 200 { - return fmt.Errorf("PUT failed: %s", res.Status) - } - return nil -} - -// progressReader wraps an io.Reader to call a progress function -// on every non-zero Read. -type progressReader struct { - r io.Reader - bytes int64 - size int64 - eof bool - progress func(n int64) -} - -func (r *progressReader) Read(p []byte) (int, error) { - n, err := r.r.Read(p) - // The request body may be read after http.Client.Do returns, see - // https://github.com/golang/go/issues/30597. Don't update progress if the - // file has been read. - r.eof = r.eof || errors.Is(err, io.EOF) - if !r.eof && r.bytes < r.size { - r.progress(int64(n)) - r.bytes += int64(n) - } - return n, err -} - -func (a *App) signOut() { - if googleClass == 0 { - return - } - err := jni.Do(a.jvm, func(env *jni.Env) error { - m := jni.GetStaticMethodID(env, googleClass, - "googleSignOut", "(Landroid/content/Context;)V") - return jni.CallStaticVoidMethod(env, googleClass, m, jni.Value(a.appCtx)) - }) - if err != nil { - fatalErr(err) - } -} - -func (a *App) googleSignIn(act jni.Object) { - if act == 0 || googleClass == 0 { - return - } - err := jni.Do(a.jvm, func(env *jni.Env) error { - sid := jni.JavaString(env, serverOAuthID) - m := jni.GetStaticMethodID(env, googleClass, - "googleSignIn", "(Landroid/app/Activity;Ljava/lang/String;I)V") - return jni.CallStaticVoidMethod(env, googleClass, m, - jni.Value(act), jni.Value(sid), jni.Value(requestSignin)) - }) - if err != nil { - fatalErr(err) - } -} - -func (a *App) browseToURL(act jni.Object, url string) { - if act == 0 { - return - } - err := jni.Do(a.jvm, func(env *jni.Env) error { - jurl := jni.JavaString(env, url) - return a.callVoidMethod(a.appCtx, "showURL", "(Landroid/app/Activity;Ljava/lang/String;)V", jni.Value(act), jni.Value(jurl)) - }) - if err != nil { - fatalErr(err) - } -} - -func (a *App) callVoidMethod(obj jni.Object, name, sig string, args ...jni.Value) error { - if obj == 0 { - panic("invalid object") - } - return jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, obj) - m := jni.GetMethodID(env, cls, name, sig) - return jni.CallVoidMethod(env, obj, m, args...) - }) -} - -// activityForView calls View.getContext and returns a global -// reference to the result. -func (a *App) contextForView(view jni.Object) jni.Object { - if view == 0 { - panic("invalid object") - } - var ctx jni.Object - err := jni.Do(a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, view) - m := jni.GetMethodID(env, cls, "getContext", "()Landroid/content/Context;") - var err error - ctx, err = jni.CallObjectMethod(env, view, m) - ctx = jni.NewGlobalRef(env, ctx) - return err - }) - if err != nil { - panic(err) - } - return ctx -} - -// 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 fatalErr(err error) { - // TODO: expose in UI. - log.Printf("fatal error: %v", err) -} - -func randHex(n int) string { - b := make([]byte, n) - rand.Read(b) - return hex.EncodeToString(b) -} - -const multipleUsersText = "Tailscale can't start due to an Android bug when multiple users are present on this device. " + - "Please see https://tailscale.com/s/multi-user-bug for more information." diff --git a/cmd/tailscale/multitun.go b/cmd/tailscale/multitun.go deleted file mode 100644 index 4531f88..0000000 --- a/cmd/tailscale/multitun.go +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "os" - - "github.com/tailscale/wireguard-go/tun" -) - -// multiTUN implements a tun.Device that supports multiple -// underlying devices. This is necessary because Android VPN devices -// have static configurations and wgengine.NewUserspaceEngine -// assumes a single static tun.Device. -type multiTUN struct { - // devices is for adding new devices. - devices chan tun.Device - // event is the combined event channel from all active devices. - events chan tun.Event - - close chan struct{} - closeErr chan error - - reads chan ioRequest - writes chan ioRequest - mtus chan chan mtuReply - names chan chan nameReply - shutdowns chan struct{} - shutdownDone chan struct{} -} - -// tunDevice wraps and drives a single run.Device. -type tunDevice struct { - dev tun.Device - // close closes the device. - close chan struct{} - closeDone chan error - // readDone is notified when the read goroutine is done. - readDone chan struct{} -} - -type ioRequest struct { - data [][]byte - sizes []int - offset int - reply chan<- ioReply -} - -type ioReply struct { - count int - err error -} - -type mtuReply struct { - mtu int - err error -} - -type nameReply struct { - name string - err error -} - -func newTUNDevices() *multiTUN { - d := &multiTUN{ - devices: make(chan tun.Device), - events: make(chan tun.Event), - close: make(chan struct{}), - closeErr: make(chan error), - reads: make(chan ioRequest), - writes: make(chan ioRequest), - mtus: make(chan chan mtuReply), - names: make(chan chan nameReply), - shutdowns: make(chan struct{}), - shutdownDone: make(chan struct{}), - } - go d.run() - return d -} - -func (d *multiTUN) run() { - var devices []*tunDevice - // readDone is the readDone channel of the device being read from. - var readDone chan struct{} - // runDone is the closeDone channel of the device being written to. - var runDone chan error - for { - select { - case <-readDone: - // The oldest device has reached EOF, replace it. - n := copy(devices, devices[1:]) - devices = devices[:n] - if len(devices) > 0 { - // Start reading from the next device. - dev := devices[0] - readDone = dev.readDone - go d.readFrom(dev) - } - case <-runDone: - // A device completed runDevice, replace it. - if len(devices) > 0 { - dev := devices[len(devices)-1] - runDone = dev.closeDone - go d.runDevice(dev) - } - case <-d.shutdowns: - // Shut down all devices. - for _, dev := range devices { - close(dev.close) - <-dev.closeDone - <-dev.readDone - } - devices = nil - d.shutdownDone <- struct{}{} - case <-d.close: - var derr error - for _, dev := range devices { - if err := <-dev.closeDone; err != nil { - derr = err - } - } - d.closeErr <- derr - return - case dev := <-d.devices: - if len(devices) > 0 { - // Ask the most recent device to stop. - prev := devices[len(devices)-1] - close(prev.close) - } - wrap := &tunDevice{ - dev: dev, - close: make(chan struct{}), - closeDone: make(chan error), - readDone: make(chan struct{}, 1), - } - if len(devices) == 0 { - // Start using this first device. - readDone = wrap.readDone - go d.readFrom(wrap) - runDone = wrap.closeDone - go d.runDevice(wrap) - } - devices = append(devices, wrap) - case m := <-d.mtus: - r := mtuReply{mtu: defaultMTU} - if len(devices) > 0 { - dev := devices[len(devices)-1] - r.mtu, r.err = dev.dev.MTU() - } - m <- r - case n := <-d.names: - var r nameReply - if len(devices) > 0 { - dev := devices[len(devices)-1] - r.name, r.err = dev.dev.Name() - } - n <- r - } - } -} - -func (d *multiTUN) readFrom(dev *tunDevice) { - defer func() { - dev.readDone <- struct{}{} - }() - for { - select { - case r := <-d.reads: - n, err := dev.dev.Read(r.data, r.sizes, r.offset) - stop := false - if err != nil { - select { - case <-dev.close: - stop = true - err = nil - default: - } - } - r.reply <- ioReply{n, err} - if stop { - return - } - case <-d.close: - return - } - } -} - -func (d *multiTUN) runDevice(dev *tunDevice) { - defer func() { - // The documentation for https://developer.android.com/reference/android/net/VpnService.Builder#establish() - // states that "Therefore, after draining the old file - // descriptor...", but pending Reads are never unblocked - // when a new descriptor is created. - // - // Close it instead and hope that no packets are lost. - dev.closeDone <- dev.dev.Close() - }() - // Pump device events. - go func() { - for { - select { - case e := <-dev.dev.Events(): - d.events <- e - case <-dev.close: - return - } - } - }() - for { - select { - case w := <-d.writes: - n, err := dev.dev.Write(w.data, w.offset) - w.reply <- ioReply{n, err} - case <-dev.close: - // Device closed. - return - case <-d.close: - // Multi-device closed. - return - } - } -} - -func (d *multiTUN) add(dev tun.Device) { - d.devices <- dev -} - -func (d *multiTUN) File() *os.File { - // The underlying file descriptor is not constant on Android. - // Let's hope no-one uses it. - panic("not available on Android") -} - -func (d *multiTUN) Read(data [][]byte, sizes []int, offset int) (int, error) { - r := make(chan ioReply) - d.reads <- ioRequest{data, sizes, offset, r} - rep := <-r - return rep.count, rep.err -} - -func (d *multiTUN) Write(data [][]byte, offset int) (int, error) { - r := make(chan ioReply) - d.writes <- ioRequest{data, nil, offset, r} - rep := <-r - return rep.count, rep.err -} - -func (d *multiTUN) MTU() (int, error) { - r := make(chan mtuReply) - d.mtus <- r - rep := <-r - return rep.mtu, rep.err -} - -func (d *multiTUN) Name() (string, error) { - r := make(chan nameReply) - d.names <- r - rep := <-r - return rep.name, rep.err -} - -func (d *multiTUN) Events() <-chan tun.Event { - return d.events -} - -func (d *multiTUN) Shutdown() { - d.shutdowns <- struct{}{} - <-d.shutdownDone -} - -func (d *multiTUN) Close() error { - close(d.close) - return <-d.closeErr -} - -func (d *multiTUN) BatchSize() int { - // TODO(raggi): currently Android disallows the necessary ioctls to enable - // batching. File a bug. - return 1 -} diff --git a/cmd/tailscale/pprof.go b/cmd/tailscale/pprof.go deleted file mode 100644 index e221874..0000000 --- a/cmd/tailscale/pprof.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -//go:build pprof -// +build pprof - -package main - -import ( - "net/http" - _ "net/http/pprof" -) - -func init() { - go func() { - http.ListenAndServe(":6060", nil) - }() -} diff --git a/cmd/tailscale/store.go b/cmd/tailscale/store.go deleted file mode 100644 index e3f62d4..0000000 --- a/cmd/tailscale/store.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "encoding/base64" - - "tailscale.com/ipn" - - "github.com/tailscale/tailscale-android/cmd/jni" -) - -// stateStore is the Go interface for a persistent storage -// backend by androidx.security.crypto.EncryptedSharedPreferences (see -// App.java). -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 -} - -func newStateStore(jvm *jni.JVM, appCtx jni.Object) *stateStore { - s := &stateStore{ - jvm: jvm, - appCtx: appCtx, - } - jni.Do(jvm, func(env *jni.Env) error { - appCls := jni.GetObjectClass(env, appCtx) - s.encrypt = jni.GetMethodID( - env, appCls, - "encryptToPref", "(Ljava/lang/String;Ljava/lang/String;)V", - ) - s.decrypt = jni.GetMethodID( - env, appCls, - "decryptFromPref", "(Ljava/lang/String;)Ljava/lang/String;", - ) - return nil - }) - return s -} - -func prefKeyFor(id ipn.StateKey) string { - return "statestore-" + string(id) -} - -func (s *stateStore) ReadString(key string, def string) (string, error) { - data, err := s.read(key) - if err != nil { - return def, err - } - if data == nil { - return def, nil - } - return string(data), nil -} - -func (s *stateStore) WriteString(key string, val string) error { - return s.write(key, []byte(val)) -} - -func (s *stateStore) ReadBool(key string, def bool) (bool, error) { - data, err := s.read(key) - if err != nil { - return def, err - } - if data == nil { - return def, nil - } - return string(data) == "true", nil -} - -func (s *stateStore) WriteBool(key string, val bool) error { - data := []byte("false") - if val { - data = []byte("true") - } - return s.write(key, data) -} - -func (s *stateStore) ReadState(id ipn.StateKey) ([]byte, error) { - state, err := s.read(prefKeyFor(id)) - if err != nil { - return nil, err - } - if state == nil { - return nil, ipn.ErrStateNotExist - } - return state, nil -} - -func (s *stateStore) WriteState(id ipn.StateKey, bs []byte) error { - prefKey := prefKeyFor(id) - return s.write(prefKey, bs) -} - -func (s *stateStore) read(key string) ([]byte, error) { - var data []byte - err := jni.Do(s.jvm, func(env *jni.Env) error { - jfile := jni.JavaString(env, key) - plain, err := jni.CallObjectMethod(env, s.appCtx, s.decrypt, - jni.Value(jfile)) - if err != nil { - return err - } - b64 := jni.GoString(env, jni.String(plain)) - if b64 == "" { - return nil - } - data, err = base64.RawStdEncoding.DecodeString(b64) - return err - }) - return data, err -} - -func (s *stateStore) write(key string, value []byte) error { - bs64 := base64.RawStdEncoding.EncodeToString(value) - err := jni.Do(s.jvm, func(env *jni.Env) error { - jfile := jni.JavaString(env, key) - jplain := jni.JavaString(env, bs64) - err := jni.CallVoidMethod(env, s.appCtx, s.encrypt, - jni.Value(jfile), jni.Value(jplain)) - if err != nil { - return err - } - return nil - }) - return err -} diff --git a/cmd/tailscale/syspolicy_handler.go b/cmd/tailscale/syspolicy_handler.go deleted file mode 100644 index 6718f37..0000000 --- a/cmd/tailscale/syspolicy_handler.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "log" - - "github.com/tailscale/tailscale-android/cmd/jni" - "tailscale.com/util/syspolicy" -) - -// androidHandler is a syspolicy handler for the Android version of the Tailscale client, -// which lets the main networking code read values set via the Android RestrictionsManager. -type androidHandler struct { - a *App -} - -func (h androidHandler) ReadString(key string) (string, error) { - if key == "" { - return "", syspolicy.ErrNoSuchKey - } - retVal := "" - err := jni.Do(h.a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, h.a.appCtx) - m := jni.GetMethodID(env, cls, "getSyspolicyStringValue", "(Ljava/lang/String;)Ljava/lang/String;") - strObj, err := jni.CallObjectMethod(env, h.a.appCtx, m, jni.Value(jni.JavaString(env, key))) - if err != nil { - return err - } - retVal = jni.GoString(env, jni.String(strObj)) - return nil - }) - if err != nil { - log.Printf("syspolicy: failed to get string value via JNI: %v", err) - } - return retVal, err -} - -func (h androidHandler) ReadBoolean(key string) (bool, error) { - if key == "" { - return false, syspolicy.ErrNoSuchKey - } - retVal := false - err := jni.Do(h.a.jvm, func(env *jni.Env) error { - cls := jni.GetObjectClass(env, h.a.appCtx) - m := jni.GetMethodID(env, cls, "getSyspolicyBooleanValue", "(Ljava/lang/String;)Z") - b, err := jni.CallBooleanMethod(env, h.a.appCtx, m, jni.Value(jni.JavaString(env, key))) - retVal = b - return err - }) - if err != nil { - log.Printf("syspolicy: failed to get bool value via JNI: %v", err) - } - return retVal, err -} - -func (h androidHandler) ReadUInt64(key string) (uint64, error) { - if key == "" { - return 0, syspolicy.ErrNoSuchKey - } - // TODO(angott): drop ReadUInt64 everywhere. We are not using it. - log.Fatalf("ReadUInt64 is not implemented on Android") - return 0, nil -} diff --git a/cmd/tailscale/tailscale.png b/cmd/tailscale/tailscale.png deleted file mode 100644 index c978560..0000000 Binary files a/cmd/tailscale/tailscale.png and /dev/null differ diff --git a/cmd/tailscale/tools.go b/cmd/tailscale/tools.go deleted file mode 100644 index 6267d59..0000000 --- a/cmd/tailscale/tools.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -//go:build tools -// +build tools - -package main - -import ( - _ "gioui.org/cmd/gogio" -) diff --git a/cmd/tailscale/ui.go b/cmd/tailscale/ui.go deleted file mode 100644 index 1040039..0000000 --- a/cmd/tailscale/ui.go +++ /dev/null @@ -1,1612 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "bytes" - "context" - "fmt" - "image" - "image/color" - "math" - "net/netip" - "time" - - "gioui.org/f32" - "gioui.org/font/opentype" - "gioui.org/io/pointer" - "gioui.org/io/system" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/op/paint" - "gioui.org/text" - "gioui.org/unit" - "gioui.org/widget" - "gioui.org/widget/material" - qrcode "github.com/skip2/go-qrcode" - "golang.org/x/exp/shiny/materialdesign/icons" - "tailscale.com/client/tailscale/apitype" - "tailscale.com/ipn" - "tailscale.com/tailcfg" - "tailscale.com/version" - - _ "embed" - - "eliasnaur.com/font/roboto/robotobold" - "eliasnaur.com/font/roboto/robotoregular" - - _ "image/png" -) - -type UI struct { - theme *material.Theme - store *stateStore - - // root is the scrollable list of the main UI. - root layout.List - // enabled is the switch for enabling or disabling the VPN. - enabled widget.Bool - search widget.Editor - - exitLAN widget.Bool - - // webSigin is the button for the web-based sign-in flow. - webSignin widget.Clickable - - // googleSignin is the button for native Google Sign-in. - googleSignin widget.Clickable - - // openExitDialog opens the exit node picker. - openExitDialog widget.Clickable - - signinType signinType - - setLoginServer bool - loginServer widget.Editor - loginServerSave widget.Clickable - loginServerCancel widget.Clickable - - self widget.Clickable - peers []widget.Clickable - - // exitDialog is state for the exit node dialog. - exitDialog struct { - show bool - dismiss Dismiss - exits widget.Enum - list layout.List - } - - runningExit bool // are we an exit node now? - - qr struct { - show bool - op paint.ImageOp - } - - intro struct { - list layout.List - start widget.Clickable - show bool - } - - menu struct { - open widget.Clickable - dismiss Dismiss - show bool - showHistory []showChange - showDebugMenu bool - - useLoginServer widget.Clickable - copy widget.Clickable - reauth widget.Clickable - bug widget.Clickable - beExit widget.Clickable - exits widget.Clickable - about widget.Clickable - logout widget.Clickable - } - - // The current pop-up message, if any - message struct { - text string - // t0 is the time when the most recent message appeared. - t0 time.Time - } - - shareDialog struct { - show bool - dismiss Dismiss - list layout.List - // peers are the nodes ready to receive files. - targets []shareTarget - loaded bool - error error - } - - // aboutDialog is state for the about dialog. - aboutDialog struct { - show bool - dismiss Dismiss - } - - // ossLicenses is the button to show the OSS licenses. - ossLicenses widget.Clickable - - icons struct { - search *widget.Icon - more *widget.Icon - exitStatus *widget.Icon - done *widget.Icon - error *widget.Icon - logo paint.ImageOp - google paint.ImageOp - } -} - -type shareTarget struct { - btn widget.Clickable - target *apitype.FileTarget - info FileSendInfo - cancel func() - updates <-chan FileSendInfo -} - -type signinType uint8 - -// An UIPeer is either a peer or a section header -// with the user information. -type UIPeer struct { - // Owner of the peer. - Owner tailcfg.UserID - // Name is the owner's name in all caps (for section headers). - Name string - // Peer is nil for section headers. - Peer *tailcfg.Node -} - -// menuItem describes an item in a popup menu. -type menuItem struct { - title string - btn *widget.Clickable -} - -const ( - headerColor = 0x496495 - infoColor = 0x3a517b - white = 0xffffff -) - -const ( - keyShowIntro = "ui.showintro" -) - -const ( - noSignin signinType = iota - webSignin - googleSignin -) - -type ( - C = layout.Context - D = layout.Dimensions -) - -var ( - //go:embed tailscale.png - tailscaleLogo []byte - //go:embed google.png - googleLogo []byte -) - -func newUI(store *stateStore) (*UI, error) { - searchIcon, err := widget.NewIcon(icons.ActionSearch) - if err != nil { - return nil, err - } - moreIcon, err := widget.NewIcon(icons.NavigationMoreVert) - if err != nil { - return nil, err - } - exitStatus, err := widget.NewIcon(icons.NavigationMenu) - if err != nil { - return nil, err - } - doneIcon, err := widget.NewIcon(icons.ActionCheckCircle) - if err != nil { - return nil, err - } - errorIcon, err := widget.NewIcon(icons.AlertErrorOutline) - if err != nil { - return nil, err - } - logo, _, err := image.Decode(bytes.NewReader(tailscaleLogo)) - if err != nil { - return nil, err - } - google, _, err := image.Decode(bytes.NewReader(googleLogo)) - if err != nil { - return nil, err - } - face, err := opentype.Parse(robotoregular.TTF) - if err != nil { - panic(fmt.Sprintf("failed to parse font: %v", err)) - } - faceBold, err := opentype.Parse(robotobold.TTF) - if err != nil { - panic(fmt.Sprintf("failed to parse font: %v", err)) - } - fonts := []text.FontFace{ - {Font: text.Font{Typeface: "Roboto"}, Face: face}, - {Font: text.Font{Typeface: "Roboto", Weight: text.Bold}, Face: faceBold}, - } - ui := &UI{ - theme: material.NewTheme(fonts), - store: store, - } - ui.intro.show, _ = store.ReadBool(keyShowIntro, true) - ui.icons.search = searchIcon - ui.icons.more = moreIcon - ui.icons.exitStatus = exitStatus - ui.icons.done = doneIcon - ui.icons.error = errorIcon - ui.icons.logo = paint.NewImageOp(logo) - ui.icons.google = paint.NewImageOp(google) - ui.root.Axis = layout.Vertical - ui.intro.list.Axis = layout.Vertical - ui.search.SingleLine = true - ui.loginServer.SingleLine = true - ui.exitDialog.list.Axis = layout.Vertical - ui.shareDialog.list.Axis = layout.Vertical - - // If they've ever set the control plane, give them the debug menu right away. - if v, _ := ui.store.ReadString(customLoginServerPrefKey, ""); v != "" { - ui.menu.showDebugMenu = true - } - - return ui, nil -} - -func mulAlpha(c color.NRGBA, alpha uint8) color.NRGBA { - c.A = uint8(uint32(c.A) * uint32(alpha) / 0xff) - return c -} - -func (ui *UI) onBack() bool { - b := ui.activeDialog() - if b == nil { - return false - } - *b = false - return true -} - -func (ui *UI) activeDialog() *bool { - switch { - case ui.qr.show: - return &ui.qr.show - case ui.menu.show: - return &ui.menu.show - case ui.shareDialog.show: - return &ui.shareDialog.show - case ui.exitDialog.show: - return &ui.exitDialog.show - case ui.aboutDialog.show: - return &ui.aboutDialog.show - } - return nil -} - -type showChange struct { - at time.Time - shown bool -} - -func (ui *UI) setMenuShown(v bool) { - if v == ui.menu.show { - return - } - ui.menu.show = v - - now := time.Now() - const recent = 5 * time.Second - recentHistory := ui.menu.showHistory[:0] - for _, hi := range ui.menu.showHistory { - if now.Sub(hi.at) < recent { - recentHistory = append(recentHistory, hi) - } - } - ui.menu.showHistory = append(recentHistory, showChange{ - at: now, - shown: v, - }) - if len(ui.menu.showHistory) >= 6 { - ui.menu.showDebugMenu = true - } -} - -func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientState) []UIEvent { - // "Get started". - if ui.intro.show { - if ui.intro.start.Clicked() { - ui.store.WriteBool(keyShowIntro, false) - ui.intro.show = false - } - ui.layoutIntro(gtx, sysIns) - return nil - } - - var events []UIEvent - - if ui.enabled.Changed() { - events = append(events, ConnectEvent{Enable: ui.enabled.Value}) - } - - for _, e := range ui.search.Events() { - if _, ok := e.(widget.ChangeEvent); ok { - events = append(events, SearchEvent{Query: ui.search.Text()}) - break - } - } - for ui.menu.open.Clicked() { - ui.setMenuShown(!ui.menu.show) - } - - netmap := state.backend.NetworkMap - var ( - localName, localAddr string - expiry time.Time - userID tailcfg.UserID - exitID tailcfg.StableNodeID - ) - if netmap != nil { - userID = netmap.User() - expiry = netmap.Expiry - localName = netmap.SelfNode.DisplayName(false) - if addrs := netmap.GetAddresses(); addrs.Len() > 0 { - localAddr = addrs.At(0).Addr().String() - } - } - if p := state.backend.Prefs; p != nil { - exitID = p.ExitNodeID - } - if d := &ui.exitDialog; d.show { - if newID := tailcfg.StableNodeID(d.exits.Value); newID != exitID { - d.show = false - events = append(events, RouteAllEvent{newID}) - } - } else { - d.exits.Value = string(exitID) - } - if ui.exitLAN.Changed() { - events = append(events, ExitAllowLANEvent(ui.exitLAN.Value)) - } - - if ui.googleSignin.Clicked() { - ui.signinType = googleSignin - events = append(events, GoogleAuthEvent{}) - } - - if ui.webSignin.Clicked() { - ui.signinType = webSignin - events = append(events, WebAuthEvent{}) - } - - if ui.loginServerSave.Clicked() { - text := ui.loginServer.Text() - ui.showMessage(gtx, "Login server saved") - events = append(events, SetLoginServerEvent{URL: text}) - ui.setLoginServer = false - } - if ui.loginServerCancel.Clicked() { - ui.setLoginServer = false - } - - if ui.menuClicked(&ui.menu.useLoginServer) { - ui.setLoginServer = true - savedLoginServer, _ := ui.store.ReadString(customLoginServerPrefKey, "") - ui.loginServer.SetText(savedLoginServer) - } - - if ui.menuClicked(&ui.menu.copy) && localAddr != "" { - events = append(events, CopyEvent{Text: localAddr}) - ui.showCopied(gtx, localAddr) - } - - if ui.menuClicked(&ui.menu.reauth) { - events = append(events, ReauthEvent{}) - } - - if ui.menuClicked(&ui.menu.bug) { - events = append(events, BugEvent{}) - ui.showCopied(gtx, "bug report marker to clipboard") - } - - if ui.menuClicked(&ui.menu.beExit) { - ui.runningExit = !ui.runningExit - events = append(events, BeExitNodeEvent(ui.runningExit)) - if ui.runningExit { - ui.showMessage(gtx, "Running exit node") - } else { - ui.showMessage(gtx, "Stopped running exit node") - } - } - - if ui.menuClicked(&ui.menu.exits) || ui.openExitDialog.Clicked() { - ui.exitDialog.show = true - } - - if ui.menuClicked(&ui.menu.about) { - ui.aboutDialog.show = true - } - - if ui.ossLicenses.Clicked() { - events = append(events, OSSLicensesEvent{}) - } - - if ui.menuClicked(&ui.menu.logout) { - events = append(events, LogoutEvent{}) - } - - for i := range ui.shareDialog.targets { - t := &ui.shareDialog.targets[i] - select { - case t.info = <-t.updates: - default: - } - if !t.btn.Clicked() { - continue - } - switch t.info.State { - case FileSendTransferring, FileSendConnecting: - t.cancel() - t.info.State = FileSendNotStarted - t.updates = nil - continue - } - t.info = FileSendInfo{ - State: FileSendConnecting, - } - ctx, cancel := context.WithCancel(context.Background()) - t.cancel = cancel - updates := make(chan FileSendInfo, 1) - t.updates = updates - events = append(events, FileSendEvent{ - Target: t.target, - Context: ctx, - Updates: func(info FileSendInfo) { - select { - case <-updates: - default: - } - updates <- info - }, - }) - } - - for len(ui.peers) < len(state.Peers) { - ui.peers = append(ui.peers, widget.Clickable{}) - } - if max := len(state.Peers); len(ui.peers) > max { - ui.peers = ui.peers[:max] - } - - const numHeaders = 6 - n := numHeaders + len(state.Peers) - needsLogin := state.backend.State == ipn.NeedsLogin - if !needsLogin { - ui.qr.show = false - } - rootGtx := gtx - if ui.activeDialog() != nil { - rootGtx.Queue = nil - } - ui.root.Layout(rootGtx, n, func(gtx C, idx int) D { - var in layout.Inset - if idx == n-1 { - // The last list element includes the bottom system - // inset. - in.Bottom = sysIns.Bottom - } - return in.Layout(gtx, func(gtx C) D { - switch idx { - case 0: - return ui.layoutTop(gtx, sysIns, &state.backend) - case 1: - if netmap == nil || state.backend.State < ipn.Stopped { - return D{} - } - for ui.self.Clicked() { - events = append(events, CopyEvent{Text: localAddr}) - ui.showCopied(gtx, localAddr) - } - return ui.layoutLocal(gtx, sysIns, localName, localAddr) - case 2: - return ui.layoutExitStatus(gtx, &state.backend) - case 3: - if state.backend.State < ipn.Stopped { - return D{} - } - return ui.layoutSearchbar(gtx, sysIns) - case 4: - if !needsLogin { - return D{} - } - return ui.layoutSignIn(gtx, &state.backend) - case 5: - // Formerly "No internet connection", which has been removed. - return D{} - default: - if needsLogin { - return D{} - } - pidx := idx - numHeaders - p := &state.Peers[pidx] - if p.Peer == nil { - name := p.Name - if p.Owner == userID { - name = "MY DEVICES" - } - return ui.layoutSection(gtx, sysIns, name) - } else { - clk := &ui.peers[pidx] - if clk.Clicked() { - if addrs := p.Peer.Addresses; len(addrs) > 0 { - a := addrs[0].Addr().String() - events = append(events, CopyEvent{Text: a}) - ui.showCopied(gtx, a) - } - } - - if p.Peer.Hostinfo.Location() != nil && p.Peer.IsWireGuardOnly { - // If the peer has location information set and is a wireguard - // only peer then it should not be displayed in the main list. - return D{} - } - return ui.layoutPeer(gtx, sysIns, p, userID, clk) - } - } - }) - }) - - ui.layoutExitNodeDialog(gtx, sysIns, state.backend.Exits) - - ui.layoutShareDialog(gtx, sysIns) - - ui.layoutAboutDialog(gtx, sysIns) - - // Popup messages. - ui.layoutMessage(gtx, sysIns) - - // 3-dots menu. - if ui.menu.show { - ui.layoutMenu(gtx, sysIns, expiry, exitID != "" || len(state.backend.Exits) > 0, needsLogin) - } - - if ui.qr.show { - ui.layoutQR(gtx, sysIns) - } - - return events -} - -func (ui *UI) layoutQR(gtx layout.Context, sysIns system.Insets) layout.Dimensions { - fill{rgb(0x232323)}.Layout(gtx, gtx.Constraints.Max) - return layout.Center.Layout(gtx, func(gtx C) D { - return drawImage(gtx, ui.qr.op, unit.Dp(300)) - }) -} - -func (ui *UI) FillShareDialog(targets []*apitype.FileTarget, err error) { - ui.shareDialog.error = err - ui.shareDialog.loaded = true - targetSet := make(map[tailcfg.NodeID]int) - if ui.shareDialog.show { - // Update rather than replace list. - for i, t := range ui.shareDialog.targets { - targetSet[t.target.Node.ID] = i - } - } else { - ui.shareDialog.targets = nil - } - for _, t := range targets { - if i, ok := targetSet[t.Node.ID]; ok { - ui.shareDialog.targets[i].target = t - } else { - ui.shareDialog.targets = append(ui.shareDialog.targets, shareTarget{target: t}) - } - } -} - -func (ui *UI) ShowShareDialog() { - ui.shareDialog.show = true -} - -func (ui *UI) ShowMessage(msg string) { - ui.message.text = msg - ui.message.t0 = time.Now() -} - -func (ui *UI) ShowQRCode(url string) { - ui.qr.show = true - q, err := qrcode.New(url, qrcode.Medium) - if err != nil { - fatalErr(err) - return - } - ui.qr.op = paint.NewImageOp(q.Image(512)) -} - -// Dismiss is a widget that detects pointer presses. -type Dismiss struct { -} - -func (d *Dismiss) Add(gtx layout.Context, color color.NRGBA) { - defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop() - pointer.InputOp{Tag: d, Types: pointer.Press}.Add(gtx.Ops) - paint.Fill(gtx.Ops, color) -} - -func (d *Dismiss) Dismissed(gtx layout.Context) bool { - for _, e := range gtx.Events(d) { - if e, ok := e.(pointer.Event); ok { - if e.Type == pointer.Press { - return true - } - } - } - return false -} - -func (ui *UI) layoutExitStatus(gtx layout.Context, state *BackendState) layout.Dimensions { - var bg color.NRGBA - var text string - switch state.ExitStatus { - case ExitNone: - return D{} - case ExitOffline: - text = "Exit node offline" - bg = rgb(0xc65835) - case ExitOnline: - text = "Using exit node" - bg = rgb(0x338b51) - } - return material.Clickable(gtx, &ui.openExitDialog, func(gtx C) D { - gtx.Constraints.Min.X = gtx.Constraints.Max.X - return layout.Stack{}.Layout(gtx, - layout.Expanded(func(gtx layout.Context) layout.Dimensions { - sz := image.Point{ - X: gtx.Constraints.Min.X, - Y: gtx.Constraints.Min.Y, - } - defer clip.Rect(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop() - paint.ColorOp{Color: bg}.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - return layout.Dimensions{Size: sz} - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - return layout.Inset{ - Top: unit.Dp(12), - Bottom: unit.Dp(12), - Right: unit.Dp(24), - Left: unit.Dp(24), - }.Layout(gtx, func(gtx C) D { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Flexed(1, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - lbl := material.Body2(ui.theme, text) - lbl.Color = rgb(white) - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx C) D { - node := material.Body2(ui.theme, state.Exit.Label) - node.Color = argb(0x88ffffff) - return node.Layout(gtx) - }), - ) - }), - layout.Rigid(func(gtx C) D { - return ui.icons.exitStatus.Layout(gtx, rgb(white)) - }), - ) - }) - }), - ) - }) -} - -// layoutSignIn lays out the sign in button(s). -func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimensions { - return layout.Inset{Top: unit.Dp(48), Left: unit.Dp(48), Right: unit.Dp(48)}.Layout(gtx, func(gtx C) D { - const ( - textColor = 0x555555 - ) - - border := widget.Border{Color: rgb(textColor), CornerRadius: unit.Dp(4), Width: unit.Dp(1)} - - if ui.setLoginServer { - return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D { - return Background{Color: rgb(0xe3e2ea), CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx C) D { - return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx C) D { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Flexed(1, - material.Editor(ui.theme, &ui.loginServer, "https://controlplane.tailscale.com").Layout, - ), - ) - }) - }) - }) - }), - layout.Rigid(func(gtx C) D { - return layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D { - return border.Layout(gtx, func(gtx C) D { - button := material.Button(ui.theme, &ui.loginServerSave, "Save") - button.Background = color.NRGBA{} // transparent - button.Color = rgb(textColor) - return button.Layout(gtx) - }) - }) - }), - layout.Rigid(func(gtx C) D { - return border.Layout(gtx, func(gtx C) D { - button := material.Button(ui.theme, &ui.loginServerCancel, "Cancel") - button.Background = color.NRGBA{} // transparent - button.Color = rgb(textColor) - return button.Layout(gtx) - }) - }), - ) - } - - return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx C) D { - if !googleSignInEnabled() { - return D{} - } - return layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D { - signin := material.ButtonLayout(ui.theme, &ui.googleSignin) - signin.Background = color.NRGBA{} // transparent - - return ui.withLoader(gtx, ui.signinType == googleSignin, func(gtx C) D { - return border.Layout(gtx, func(gtx C) D { - if ui.signinType != noSignin { - gtx.Queue = nil - } - return signin.Layout(gtx, func(gtx C) D { - gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(48)) - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Inset{Right: unit.Dp(4)}.Layout(gtx, func(gtx C) D { - return drawImage(gtx, ui.icons.google, unit.Dp(16)) - }) - }), - layout.Rigid(func(gtx C) D { - return layout.Inset{Top: unit.Dp(10), Bottom: unit.Dp(10)}.Layout(gtx, func(gtx C) D { - l := material.Body2(ui.theme, "Sign in with Google") - l.Color = rgb(textColor) - return l.Layout(gtx) - }) - }), - ) - }) - }) - }) - }) - }), - layout.Rigid(func(gtx C) D { - label := "Sign in with other" - if !googleSignInEnabled() { - label = "Sign in" - } - return ui.withLoader(gtx, ui.signinType == webSignin, func(gtx C) D { - return border.Layout(gtx, func(gtx C) D { - if ui.signinType != noSignin { - gtx.Queue = nil - } - signin := material.Button(ui.theme, &ui.webSignin, label) - signin.Background = color.NRGBA{} // transparent - signin.Color = rgb(textColor) - return signin.Layout(gtx) - }) - }) - }), - ) - }) -} - -func (ui *UI) withLoader(gtx layout.Context, loading bool, w layout.Widget) layout.Dimensions { - cons := gtx.Constraints - return layout.Stack{Alignment: layout.W}.Layout(gtx, - layout.Stacked(func(gtx C) D { - gtx.Constraints = cons - return w(gtx) - }), - layout.Stacked(func(gtx C) D { - if !loading { - return D{} - } - return layout.Inset{Left: unit.Dp(16)}.Layout(gtx, func(gtx C) D { - gtx.Constraints.Min = image.Point{ - X: gtx.Dp(unit.Dp(16)), - } - return material.Loader(ui.theme).Layout(gtx) - }) - }), - ) -} - -// layoutIntro lays out the intro page with the logo and terms. -func (ui *UI) layoutIntro(gtx layout.Context, sysIns system.Insets) { - fill{rgb(0x232323)}.Layout(gtx, gtx.Constraints.Max) - ui.intro.list.Layout(gtx, 1, func(gtx C, idx int) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - // 9 dot logo. - layout.Rigid(func(gtx C) D { - return layout.Inset{Top: unit.Dp(80), Bottom: unit.Dp(48)}.Layout(gtx, func(gtx C) D { - return layout.N.Layout(gtx, func(gtx C) D { - sz := gtx.Dp(unit.Dp(72)) - drawLogo(gtx.Ops, sz) - return layout.Dimensions{Size: image.Pt(sz, sz)} - }) - }) - }), - // "tailscale". - layout.Rigid(func(gtx C) D { - return layout.N.Layout(gtx, func(gtx C) D { - return drawImage(gtx, ui.icons.logo, unit.Dp(200)) - }) - }), - // Terms. - layout.Rigid(func(gtx C) D { - return layout.Inset{ - Top: unit.Dp(48), - Left: unit.Dp(32), - Right: unit.Dp(32), - }.Layout(gtx, func(gtx C) D { - terms := material.Body2(ui.theme, termsText) - terms.Color = rgb(0xbfbfbf) - terms.Alignment = text.Middle - return terms.Layout(gtx) - }) - }), - // "Get started". - layout.Rigid(func(gtx C) D { - return layout.Inset{ - Top: unit.Dp(16), - Left: unit.Dp(16), - Right: unit.Dp(16), - Bottom: sysIns.Bottom, - }.Layout(gtx, func(gtx C) D { - start := material.Button(ui.theme, &ui.intro.start, "Get Started") - start.Inset = layout.UniformInset(unit.Dp(16)) - start.CornerRadius = unit.Dp(16) - start.Background = rgb(0x496495) - start.TextSize = unit.Sp(20) - return start.Layout(gtx) - }) - }), - ) - }) -} - -// menuClicked is like btn.Clicked, but also closes the menu if true. -func (ui *UI) menuClicked(btn *widget.Clickable) bool { - cl := btn.Clicked() - if cl { - ui.setMenuShown(false) - } - return cl -} - -// layoutShareDialog lays out the file sharing dialog shown on file send intents (ACTION_SEND, ACTION_SEND_MULTIPLE). -func (ui *UI) layoutShareDialog(gtx layout.Context, sysIns system.Insets) { - d := &ui.shareDialog - if d.dismiss.Dismissed(gtx) { - ui.shareDialog.show = false - } - if !d.show { - return - } - d.dismiss.Add(gtx, argb(0x66000000)) - layout.Inset{ - Top: sysIns.Top + unit.Dp(16), - Right: sysIns.Right + unit.Dp(16), - Bottom: sysIns.Bottom + unit.Dp(16), - Left: sysIns.Left + unit.Dp(16), - }.Layout(gtx, func(gtx C) D { - return layout.Center.Layout(gtx, func(gtx C) D { - gtx.Constraints.Min.X = gtx.Dp(unit.Dp(250)) - gtx.Constraints.Max.X = gtx.Constraints.Min.X - return layoutDialog(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - // Header. - d := layout.Inset{ - Top: unit.Dp(16), - Right: unit.Dp(20), - Left: unit.Dp(20), - Bottom: unit.Dp(16), - }.Layout(gtx, func(gtx C) D { - l := material.Body1(ui.theme, "Share via Tailscale") - l.Font.Weight = text.Bold - return l.Layout(gtx) - }) - // Swallow clicks to title. - var c widget.Clickable - gtx.Queue = nil - return c.Layout(gtx, func(gtx C) D { return d }) - }), - layout.Rigid(func(gtx C) D { - if d.loaded { - return D{} - } - return layout.UniformInset(unit.Dp(50)).Layout(gtx, func(gtx C) D { - return layout.Center.Layout(gtx, func(gtx C) D { - sz := gtx.Dp(unit.Dp(32)) - gtx.Constraints.Min = image.Pt(sz, sz) - gtx.Constraints.Max = gtx.Constraints.Min - return material.Loader(ui.theme).Layout(gtx) - }) - }) - }), - layout.Rigid(func(gtx C) D { - if d.error == nil { - return D{} - } - sz := gtx.Dp(unit.Dp(50)) - gtx.Constraints.Min.Y = sz - return layout.UniformInset(unit.Dp(20)).Layout(gtx, func(gtx C) D { - return layout.W.Layout(gtx, func(gtx C) D { - return material.Body2(ui.theme, d.error.Error()).Layout(gtx) - }) - }) - }), - layout.Flexed(1, func(gtx C) D { - gtx.Constraints.Min.Y = 0 - return d.list.Layout(gtx, len(d.targets), func(gtx C, idx int) D { - node := &d.targets[idx] - target := node.target.Node - lbl := target.ComputedName - offline := target.Online != nil && !*target.Online - if offline { - lbl = lbl + " (offline)" - } - w := material.Body2(ui.theme, lbl) - if offline { - w.Color = rgb(0xbbbbbb) - gtx.Queue = nil - } - return material.Clickable(gtx, &node.btn, func(gtx C) D { - return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Flexed(1, w.Layout), - layout.Rigid(func(gtx C) D { - sz := gtx.Dp(unit.Dp(16)) - gtx.Constraints.Min = image.Pt(sz, sz) - switch node.info.State { - case FileSendConnecting: - return material.Loader(ui.theme).Layout(gtx) - case FileSendTransferring: - return material.ProgressCircle(ui.theme, float32(node.info.Progress)).Layout(gtx) - case FileSendFailed: - return ui.icons.error.Layout(gtx, rgb(0xcc6539)) - case FileSendComplete: - return ui.icons.done.Layout(gtx, ui.theme.Palette.ContrastBg) - default: - return D{} - } - }), - ) - }) - }) - }) - }), - ) - }) - }) - }) -} - -// layoutExitNodeDialog lays out the exit node selection dialog. -func (ui *UI) layoutExitNodeDialog(gtx layout.Context, sysIns system.Insets, exits []Peer) { - d := &ui.exitDialog - if d.dismiss.Dismissed(gtx) { - d.show = false - } - if !d.show { - return - } - d.dismiss.Add(gtx, argb(0x66000000)) - layout.Inset{ - Top: sysIns.Top + unit.Dp(16), - Right: sysIns.Right + unit.Dp(16), - Bottom: sysIns.Bottom + unit.Dp(16), - Left: sysIns.Left + unit.Dp(16), - }.Layout(gtx, func(gtx C) D { - return layout.Center.Layout(gtx, func(gtx C) D { - gtx.Constraints.Min.X = gtx.Dp(unit.Dp(250)) - gtx.Constraints.Max.X = gtx.Constraints.Min.X - return layoutDialog(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - // Header. - return layout.Inset{ - Top: unit.Dp(16), - Right: unit.Dp(20), - Left: unit.Dp(20), - Bottom: unit.Dp(16), - }.Layout(gtx, func(gtx C) D { - l := material.Body1(ui.theme, "Use exit node...") - l.Font.Weight = text.Bold - return l.Layout(gtx) - }) - }), - layout.Flexed(1, func(gtx C) D { - gtx.Constraints.Min.Y = 0 - // Add "none" exit node, then "Allow LAN" checkbox, then the exit nodes. - n := len(exits) + 2 - return d.list.Layout(gtx, n, func(gtx C, idx int) D { - if idx == 0 { - btn := material.CheckBox(ui.theme, &ui.exitLAN, "Allow LAN access") - return layout.Inset{ - Right: unit.Dp(16), - Left: unit.Dp(16), - Bottom: unit.Dp(16), - }.Layout(gtx, btn.Layout) - } - node := Peer{Label: "None", Online: true, Location: nil} - if idx >= 2 { - node = exits[idx-2] - } - lbl := node.Label - if !node.Online { - lbl = lbl + " (offline)" - } - btn := material.RadioButton(ui.theme, &d.exits, string(node.ID), lbl) - if !node.Online { - btn.Color = rgb(0xbbbbbb) - btn.IconColor = btn.Color - } - return layout.Inset{ - Right: unit.Dp(16), - Left: unit.Dp(16), - Bottom: unit.Dp(16), - }.Layout(gtx, btn.Layout) - }) - }), - ) - }) - }) - }) -} - -// layoutAboutDialog lays out the about dialog. -func (ui *UI) layoutAboutDialog(gtx layout.Context, sysIns system.Insets) { - d := &ui.aboutDialog - if d.dismiss.Dismissed(gtx) { - d.show = false - } - if !d.show { - return - } - d.dismiss.Add(gtx, argb(0x66000000)) - layout.Inset{ - Top: sysIns.Top + unit.Dp(16), - Right: sysIns.Right + unit.Dp(16), - Bottom: sysIns.Bottom + unit.Dp(16), - Left: sysIns.Left + unit.Dp(16), - }.Layout(gtx, func(gtx C) D { - return layout.Center.Layout(gtx, func(gtx C) D { - gtx.Constraints.Min.X = gtx.Dp(unit.Dp(250)) - gtx.Constraints.Max.X = gtx.Constraints.Min.X - return layoutDialog(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - // Header. - return layout.Inset{ - Top: unit.Dp(16), - Right: unit.Dp(20), - Left: unit.Dp(20), - Bottom: unit.Dp(16), - }.Layout(gtx, func(gtx C) D { - l := material.Body1(ui.theme, "About") - l.Font.Weight = text.Bold - return l.Layout(gtx) - }) - }), - layout.Rigid(func(gtx C) D { - return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D { - return material.Body1(ui.theme, "version "+version.Short()).Layout(gtx) - }) - }), - layout.Rigid(func(gtx C) D { - return material.Clickable(gtx, &ui.ossLicenses, func(gtx C) D { - return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D { - return material.Body1(ui.theme, "Open Source Licenses").Layout(gtx) - }) - }) - }), - ) - }) - }) - }) -} - -func layoutMenu(th *material.Theme, gtx layout.Context, items []menuItem, header layout.Widget) layout.Dimensions { - return layoutDialog(gtx, func(gtx C) D { - // Lay out menu items twice; once for - // measuring the widest item, once for actual layout. - var maxWidth int - var minWidth int - children := []layout.FlexChild{ - layout.Rigid(func(gtx C) D { - return layout.Inset{ - Top: unit.Dp(16), - Right: unit.Dp(16), - Left: unit.Dp(16), - Bottom: unit.Dp(4), - }.Layout(gtx, func(gtx C) D { - gtx.Constraints.Min.X = minWidth - dims := header(gtx) - if w := dims.Size.X; w > maxWidth { - maxWidth = w - } - return dims - }) - }), - } - for i := 0; i < len(items); i++ { - it := &items[i] - children = append(children, layout.Rigid(func(gtx C) D { - return material.Clickable(gtx, it.btn, func(gtx C) D { - return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D { - gtx.Constraints.Min.X = minWidth - dims := material.Body1(th, it.title).Layout(gtx) - if w := dims.Size.X; w > maxWidth { - maxWidth = w - } - return dims - }) - }) - })) - } - f := layout.Flex{Axis: layout.Vertical} - // First pass: record and discard operations - // and determine widest item. - m := op.Record(gtx.Ops) - f.Layout(gtx, children...) - m.Stop() - // Second pass: layout items with equal width. - minWidth = maxWidth - return f.Layout(gtx, children...) - }) -} - -func layoutDialog(gtx layout.Context, w layout.Widget) layout.Dimensions { - return widget.Border{Color: argb(0x33000000), CornerRadius: unit.Dp(2), Width: unit.Dp(1)}.Layout(gtx, func(gtx C) D { - return Background{Color: rgb(0xfafafa), CornerRadius: unit.Dp(2)}.Layout(gtx, w) - }) -} - -// layoutMenu lays out the menu activated by the 3 dots button. -func (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.Time, showExits bool, needsLogin bool) { - ui.menu.dismiss.Add(gtx, color.NRGBA{}) - if ui.menu.dismiss.Dismissed(gtx) { - ui.setMenuShown(false) - } - layout.Inset{ - Top: sysIns.Top + unit.Dp(2), - Right: sysIns.Right + unit.Dp(2), - }.Layout(gtx, func(gtx C) D { - return layout.NE.Layout(gtx, func(gtx C) D { - menu := &ui.menu - if ui.setLoginServer { - return D{} - } - if needsLogin { - var items []menuItem - title := "Tailscale " + version.Short() - if ui.menu.showDebugMenu { - items = append(items, menuItem{title: "Change server", btn: &menu.useLoginServer}) - } - items = append(items, menuItem{title: "About", btn: &menu.about}) - return layoutMenu(ui.theme, gtx, items, func(gtx C) D { - l := material.Caption(ui.theme, title) - return l.Layout(gtx) - }) - } - items := []menuItem{ - {title: "Copy my IP address", btn: &menu.copy}, - } - if showExits { - items = append(items, menuItem{title: "Use exit node...", btn: &menu.exits}) - } - items = append(items, - menuItem{title: "Bug report", btn: &menu.bug}, - menuItem{title: "Reauthenticate", btn: &menu.reauth}, - menuItem{title: "Log out", btn: &menu.logout}, - ) - - var title string - if ui.runningExit { - title = "Stop running exit node" - } else { - title = "Run exit node" - } - items = append(items, menuItem{title: title, btn: &menu.beExit}) - - items = append(items, menuItem{title: "About", btn: &menu.about}) - - return layoutMenu(ui.theme, gtx, items, func(gtx C) D { - var expiryStr string - const fmtStr = time.Stamp - switch { - case expiry.IsZero(): - expiryStr = "Expires: (never)" - case time.Now().After(expiry): - expiryStr = fmt.Sprintf("Expired: %s", expiry.Format(fmtStr)) - default: - expiryStr = fmt.Sprintf("Expires: %s", expiry.Format(fmtStr)) - } - l := material.Caption(ui.theme, expiryStr) - l.Color = rgb(0x8f8f8f) - return l.Layout(gtx) - }) - }) - }) -} - -func (ui *UI) layoutMessage(gtx layout.Context, sysIns system.Insets) layout.Dimensions { - s := ui.message.text - if s == "" { - return D{} - } - now := gtx.Now - d := now.Sub(ui.message.t0) - rem := 4*time.Second - d - if rem < 0 { - return D{} - } - op.InvalidateOp{At: now.Add(rem)}.Add(gtx.Ops) - return layout.S.Layout(gtx, func(gtx C) D { - return layout.Inset{ - Left: sysIns.Left + unit.Dp(8), - Right: sysIns.Right + unit.Dp(8), - Bottom: sysIns.Bottom + unit.Dp(8), - }.Layout(gtx, func(gtx C) D { - return Background{Color: rgb(0x323232), CornerRadius: unit.Dp(5)}.Layout(gtx, func(gtx C) D { - return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx C) D { - l := material.Body2(ui.theme, s) - l.Color = rgb(0xdddddd) - return l.Layout(gtx) - }) - }) - }) - }) -} - -func (ui *UI) showMessage(gtx layout.Context, msg string) { - ui.message.text = msg - ui.message.t0 = gtx.Now - op.InvalidateOp{}.Add(gtx.Ops) -} - -func MaxDp(p1, p2 unit.Dp) unit.Dp { - if p1 > p2 { - return p1 - } - return p2 -} - -// layoutPeer lays out a peer name and IP address (e.g. -// "localhost\n100.100.100.101") -func (ui *UI) layoutPeer(gtx layout.Context, sysIns system.Insets, p *UIPeer, user tailcfg.UserID, clk *widget.Clickable) layout.Dimensions { - return material.Clickable(gtx, clk, func(gtx C) D { - return layout.Inset{ - Top: unit.Dp(8), - Right: MaxDp(sysIns.Right, unit.Dp(16)), - Left: MaxDp(sysIns.Left, unit.Dp(16)), - Bottom: unit.Dp(8), - }.Layout(gtx, func(gtx C) D { - gtx.Constraints.Min.X = gtx.Constraints.Max.X - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Stack{}.Layout(gtx, - layout.Expanded(func(gtx C) D { - return layout.Stack{Alignment: layout.W}.Layout(gtx, - layout.Stacked(func(gtx C) D { - return layout.Inset{Bottom: unit.Dp(8)}.Layout(gtx, func(gtx C) D { - return layout.N.Layout(gtx, func(gtx C) D { - if p.Peer.Online != nil && *p.Peer.Online { - drawDisc(gtx.Ops, 24, rgb(0x009966)) - } else { - drawDisc(gtx.Ops, 24, rgb(0xcccccc)) - } - return layout.Dimensions{Size: image.Pt(0, 0)} - }) - }) - }), - layout.Stacked(func(gtx C) D { - return layout.Inset{Left: unit.Dp(16), Bottom: unit.Dp(4)}.Layout(gtx, func(gtx C) D { - name := p.Peer.DisplayName(p.Peer.User == user) - return material.H6(ui.theme, name).Layout(gtx) - }) - }), - ) - }), - ) - - }), - layout.Rigid(func(gtx C) D { - var bestIP netip.Addr // IP to show; first IPv4, or first IPv6 if no IPv4 - for _, addr := range p.Peer.Addresses { - if ip := addr.Addr(); !bestIP.IsValid() || bestIP.Is6() && ip.Is4() { - bestIP = ip - } - } - l := material.Body2(ui.theme, bestIP.String()) - l.Color = rgb(0x434343) - return l.Layout(gtx) - }), - ) - }) - }) -} - -// layoutSection lays out a section title (e.g. "My devices"). -func (ui *UI) layoutSection(gtx layout.Context, sysIns system.Insets, title string) layout.Dimensions { - return Background{Color: rgb(0xe1e0e9)}.Layout(gtx, func(gtx C) D { - return layout.Inset{ - Top: unit.Dp(16), - Right: MaxDp(sysIns.Right, unit.Dp(16)), - Left: MaxDp(sysIns.Left, unit.Dp(16)), - Bottom: unit.Dp(16), - }.Layout(gtx, func(gtx C) D { - l := material.Body1(ui.theme, title) - l.Color = rgb(0x6f797d) - return l.Layout(gtx) - }) - }) -} - -// layoutTop lays out the top controls: toggle, status and menu dots. -func (ui *UI) layoutTop(gtx layout.Context, sysIns system.Insets, state *BackendState) layout.Dimensions { - in := layout.Inset{ - Top: unit.Dp(16), - Bottom: unit.Dp(16), - } - return Background{Color: rgb(headerColor)}.Layout(gtx, func(gtx C) D { - return layout.Inset{ - Top: sysIns.Top, - Right: MaxDp(sysIns.Right, unit.Dp(8)), - Left: MaxDp(sysIns.Left, unit.Dp(16)), - }.Layout(gtx, func(gtx C) D { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return in.Layout(gtx, func(gtx C) D { - if state.State <= ipn.NeedsLogin { - return D{} - } - sw := material.Switch(ui.theme, &ui.enabled, "Enable VPN") - sw.Color.Enabled = rgb(white) - if state.State < ipn.Stopped { - sw.Color.Enabled = rgb(0xbbbbbb) - sw.Color.Disabled = rgb(0xbbbbbb) - } - return sw.Layout(gtx) - }) - }), - layout.Flexed(1, func(gtx C) D { - return in.Layout(gtx, func(gtx C) D { - return layout.Inset{Left: unit.Dp(16)}.Layout(gtx, func(gtx C) D { - lbl := material.Body1(ui.theme, statusString(state.State)) - lbl.Color = rgb(0xffffff) - return lbl.Layout(gtx) - }) - }) - }), - layout.Rigid(func(gtx C) D { - btn := material.IconButton(ui.theme, &ui.menu.open, ui.icons.more, "Open menu") - btn.Color = rgb(white) - btn.Background = color.NRGBA{} - return btn.Layout(gtx) - }), - ) - }) - }) -} - -func statusString(state ipn.State) string { - switch state { - case ipn.Stopped: - return "Stopped" - case ipn.Starting: - return "Starting..." - case ipn.Running: - return "Active" - case ipn.NeedsMachineAuth: - return "Awaiting Approval" - case ipn.NeedsLogin: - return "Tailscale" - default: - return "Loading..." - } -} - -func (ui *UI) showCopied(gtx layout.Context, addr string) { - ui.showMessage(gtx, fmt.Sprintf("Copied %s", addr)) -} - -// layoutLocal lays out the information box about the local node's -// name and IP address. -func (ui *UI) layoutLocal(gtx layout.Context, sysIns system.Insets, host, addr string) layout.Dimensions { - return Background{Color: rgb(headerColor)}.Layout(gtx, func(gtx C) D { - return layout.Inset{ - Right: MaxDp(sysIns.Right, unit.Dp(8)), - Left: MaxDp(sysIns.Left, unit.Dp(8)), - Bottom: unit.Dp(8), - }.Layout(gtx, func(gtx C) D { - return Background{Color: rgb(infoColor), CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx C) D { - return material.Clickable(gtx, &ui.self, func(gtx C) D { - return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D { - gtx.Constraints.Min.X = gtx.Constraints.Max.X - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx C) D { - name := material.H6(ui.theme, host) - name.Color = rgb(0xffffff) - return name.Layout(gtx) - }) - }), - layout.Rigid(func(gtx C) D { - name := material.Body2(ui.theme, addr) - name.Color = rgb(0xc5ccd9) - return name.Layout(gtx) - }), - ) - }) - }) - }) - }) - }) -} - -func (ui *UI) layoutSearchbar(gtx layout.Context, sysIns system.Insets) layout.Dimensions { - return Background{Color: rgb(0xf0eff6)}.Layout(gtx, func(gtx C) D { - return layout.Inset{ - Top: unit.Dp(8), - Right: MaxDp(sysIns.Right, unit.Dp(8)), - Left: MaxDp(sysIns.Left, unit.Dp(8)), - Bottom: unit.Dp(8), - }.Layout(gtx, func(gtx C) D { - return Background{Color: rgb(0xe3e2ea), CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx C) D { - return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx C) D { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx C) D { - col := mulAlpha(ui.theme.Palette.Fg, 0xbb) - return ui.icons.search.Layout(gtx, col) - }), - layout.Flexed(1, - material.Editor(ui.theme, &ui.search, "Search by device name...").Layout, - ), - ) - }) - }) - }) - }) -} - -// drawLogo draws the Tailscale logo using vector operations. -func drawLogo(ops *op.Ops, size int) { - scale := float64(size) / 680 - discDia := float32(170 * scale) - off := int(math.Round(172 * 1.5 * scale)) - tx := op.Offset(image.Pt(off, 0)) - ty := op.Offset(image.Pt(0, off)) - - defer op.Offset(image.Point{}).Push(ops).Pop() - - // First row of discs. - row := op.Offset(image.Point{}).Push(ops) - drawDisc(ops, discDia, rgb(0x54514d)) - tx.Add(ops) - drawDisc(ops, discDia, rgb(0x54514d)) - tx.Add(ops) - drawDisc(ops, discDia, rgb(0x54514d)) - row.Pop() - - ty.Add(ops) - // Second row. - row = op.Offset(image.Point{}).Push(ops) - drawDisc(ops, discDia, rgb(0xfffdfa)) - tx.Add(ops) - drawDisc(ops, discDia, rgb(0xfffdfa)) - tx.Add(ops) - drawDisc(ops, discDia, rgb(0xfffdfa)) - row.Pop() - - ty.Add(ops) - // Third row. - row = op.Offset(image.Point{}).Push(ops) - drawDisc(ops, discDia, rgb(0xfffdfa)) - drawDisc(ops, discDia, rgb(0x54514d)) - tx.Add(ops) - drawDisc(ops, discDia, rgb(0xfffdfa)) - tx.Add(ops) - drawDisc(ops, discDia, rgb(0x54514d)) - row.Pop() -} - -func drawImage(gtx layout.Context, img paint.ImageOp, size unit.Dp) layout.Dimensions { - img.Add(gtx.Ops) - sz := img.Size() - aspect := float32(sz.Y) / float32(sz.X) - w := gtx.Dp(size) - h := int(float32(w)*aspect + .5) - scale := float32(w) / float32(sz.X) - defer op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Point{X: scale, Y: scale})).Push(gtx.Ops).Pop() - paint.PaintOp{}.Add(gtx.Ops) - return layout.Dimensions{Size: image.Pt(w, h)} -} - -func drawDisc(ops *op.Ops, radius float32, col color.NRGBA) { - r := int(math.Round(float64(radius))) - r2 := int(math.Round(float64(radius * .5))) - defer clip.RRect{ - Rect: image.Rect(0, 0, r, r), - NE: r2, NW: r2, SE: r2, SW: r2, - }.Push(ops).Pop() - paint.ColorOp{Color: col}.Add(ops) - paint.PaintOp{}.Add(ops) -} - -// Background lays out a widget and draws a color background behind it. -type Background struct { - Color color.NRGBA - CornerRadius unit.Dp -} - -func (b Background) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - m := op.Record(gtx.Ops) - dims := w(gtx) - sz := dims.Size - call := m.Stop() - // Clip corners, if any. - if r := gtx.Dp(b.CornerRadius); r > 0 { - defer clip.RRect{ - Rect: image.Rect(0, 0, sz.X, sz.Y), - NE: r, NW: r, SE: r, SW: r, - }.Push(gtx.Ops).Pop() - } - fill{b.Color}.Layout(gtx, sz) - call.Add(gtx.Ops) - return dims -} - -type fill struct { - col color.NRGBA -} - -func (f fill) Layout(gtx layout.Context, sz image.Point) layout.Dimensions { - defer clip.Rect(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop() - paint.ColorOp{Color: f.col}.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - return layout.Dimensions{Size: sz} -} - -func rgb(c uint32) color.NRGBA { - return argb((0xff << 24) | c) -} - -func argb(c uint32) color.NRGBA { - return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)} -} - -const termsText = `Tailscale is a mesh VPN for securely connecting your devices. All connections are device-to-device, so we never see your data. - -We collect and use your email address and name, as well as your device name, OS version, and IP address in order to help you to connect your devices and manage your settings. We log when you are connected to your network.` diff --git a/go.mod b/go.mod index 81e76f4..809e6a1 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,7 @@ module github.com/tailscale/tailscale-android go 1.22.0 require ( - eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 - gioui.org v0.0.0-20230206180804-32c6a9b10d0b - gioui.org/cmd v0.0.0-20210925100615-41f3a7e74ee6 - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tailscale/wireguard-go v0.0.0-20240413175505-64040e66467d - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a - golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87 golang.org/x/sys v0.18.0 inet.af/netaddr v0.0.0-20220617031823-097006376321 @@ -18,9 +12,6 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect - gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect - gioui.org/shader v1.0.6 // indirect - github.com/akavel/rsrc v0.10.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect @@ -37,7 +28,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect github.com/aws/smithy-go v1.19.0 // indirect - github.com/benoitkugler/textlayout v0.3.0 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/coreos/go-iptables v0.7.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -47,7 +37,6 @@ require ( github.com/gaissmai/bart v0.4.1 // indirect github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect @@ -90,7 +79,7 @@ require ( go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/image v0.15.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/sync v0.6.0 // indirect diff --git a/go.sum b/go.sum index 8c1ef1c..2bf89a2 100644 --- a/go.sum +++ b/go.sum @@ -1,49 +1,11 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 h1:djFprmHZgrSepsHAIRMp5UJn3PzsoTg9drI+BDmif5Q= -eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= -gioui.org v0.0.0-20210910062418-d5d0a75a9bcb/go.mod h1:BTldRXnY5mrUrYZCdWyDwyMzyUzpfZN1cF4MMRrOt9w= -gioui.org v0.0.0-20230206180804-32c6a9b10d0b h1:ghRvtb24ItyA7QffjaG38gH5f7bzagcz0OPq3T3FyHI= -gioui.org v0.0.0-20230206180804-32c6a9b10d0b/go.mod h1:3lLo7xMHYnnHTrgKNNctBjEKKH3wQCO2Sn7ti5Jy8mU= -gioui.org/cmd v0.0.0-20210925100615-41f3a7e74ee6 h1:SkAdohDhTUjl+ZtM417Xeu+uFd7SQubwR9uAyqJqC8c= -gioui.org/cmd v0.0.0-20210925100615-41f3a7e74ee6/go.mod h1:qrH3h4nt/PyIqx/XabL/eJ5cXQnQ0ERHqC3VEXx/Rmg= -gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= -gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= -gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= -gioui.org/shader v1.0.2/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= -gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= -gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o= -github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw= @@ -72,223 +34,82 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGz github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE= -github.com/benoitkugler/textlayout v0.3.0 h1:2ehWXEkgb6RUokTjXh1LzdGwG4dRP6X3dqhYYDYhUVk= -github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w= -github.com/benoitkugler/textlayout-testdata v0.1.1 h1:AvFxBxpfrQd8v55qH59mZOJOQjtD6K2SFe9/HvnIbJk= -github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg= -github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= -github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194= -github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA= github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls= github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 h1:iOA0HmtpANn48hX2nlDNMu0VVaNza35HJG0WeetBVzQ= -github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5/go.mod h1:/cmOXaoTiO+lbCwkTZBgCvevJpbFsZ5reXIpEJVh5MI= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs= -github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -297,113 +118,22 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= @@ -430,11 +160,8 @@ github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw= github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -442,30 +169,7 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= @@ -476,101 +180,36 @@ go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1: go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20210722180016-6781d3edade3/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 h1:3AGKexOYqL+ztdWdkB1bDwXgPBuTS/S8A4WzuTvJ8Cg= -golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87 h1:dm00oNtDy265HReLTARPfIDXTRb2IG0jqQVpn7p5MKE= golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87/go.mod h1:DN+F2TpepQEh5goqWnM3gopfFakSWM8OmHiz0rPRjT4= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -578,99 +217,41 @@ golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepC golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM= gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU= inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= tailscale.com v1.65.0-pre.0.20240417162431-449be38e0381 h1:Wiz4SeARcNVLO7rmZy0mPj0RJHcSdHIQ8itrPkgNKyo= tailscale.com v1.65.0-pre.0.20240417162431-449be38e0381/go.mod h1:PVwaayzADTOctljVKj+M50OtQ0dOGYBLi2fdbOxo6vw=