Repository: ppyyr/tailscale-android Branch: main Commit: 6030dd3fb5ef Files: 43 Total size: 276.5 KB Directory structure: gitextract_xph_jeqj/ ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PATENTS ├── README.md ├── android/ │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── tailscale/ │ │ │ └── ipn/ │ │ │ ├── App.java │ │ │ ├── DnsConfig.java │ │ │ ├── IPNActivity.java │ │ │ ├── IPNService.java │ │ │ ├── Peer.java │ │ │ └── QuickToggleService.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_tile.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── ic_launcher_background.xml │ │ └── strings.xml │ ├── play/ │ │ └── java/ │ │ └── com/ │ │ └── tailscale/ │ │ └── ipn/ │ │ └── Google.java │ └── test/ │ └── java/ │ └── com/ │ └── tailscale/ │ └── ipn/ │ └── DnsConfigTest.java ├── cmd/ │ └── tailscale/ │ ├── backend.go │ ├── callbacks.go │ ├── main.go │ ├── multitun.go │ ├── pprof.go │ ├── store.go │ ├── tools.go │ └── ui.go ├── flake.nix ├── go.mod ├── go.sum ├── jni/ │ └── jni.go ├── metadata/ │ └── en-US/ │ ├── full_description.txt │ └── short_description.txt └── version/ └── tailscale-version.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/main.yml ================================================ # This is a basic workflow to help you get started with Actions name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the main branch push: branches: [ main ] pull_request: branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 # Runs a single command using the runners shell - name: Create Docker container run: docker build . --file Dockerfile --tag tailscale-android # Runs a set of commands using the runners shell - name: build tailscale-fdroid.apk run: docker run -v /home/runner/work/tailscale-android/tailscale-android:/build/tailscale-android tailscale-android make tailscale-fdroid.apk - name: Upload a Build Artifact uses: actions/upload-artifact@v3.0.0 with: # Artifact name name: tailscale-fdroid.apk # optional, default is artifact # A file, directory or wildcard pattern that describes what to upload path: /home/runner/work/tailscale-android/tailscale-android/tailscale-fdroid.apk # The desired behavior if no files are found using the provided path. if-no-files-found: error # optional, default is warn retention-days: 30 # optional ================================================ FILE: .gitignore ================================================ # Ignore Gradle project-specific cache directory .gradle # Ignore Gradle build output directory build # The destination for the Go Android archive. android/libs # Output files from the Makefile: tailscale-debug.apk ================================================ FILE: Dockerfile ================================================ # This is a Dockerfile for creating a build environment for # tailscale-android. FROM openjdk:8-jdk # To enable running android tools such as aapt RUN apt-get update && apt-get -y upgrade RUN apt-get install -y lib32z1 lib32stdc++6 # For Go: RUN apt-get -y --no-install-recommends install curl gcc RUN apt-get -y --no-install-recommends install ca-certificates libc6-dev git RUN apt-get -y install make RUN mkdir -p BUILD ENV HOME /build # Get android sdk, ndk, and rest of the stuff needed to build the android app. WORKDIR $HOME RUN mkdir android-sdk ENV ANDROID_HOME $HOME/android-sdk WORKDIR $ANDROID_HOME RUN curl -O https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip RUN echo '444e22ce8ca0f67353bda4b85175ed3731cae3ffa695ca18119cbacef1c1bea0 sdk-tools-linux-3859397.zip' | sha256sum -c RUN unzip sdk-tools-linux-3859397.zip RUN echo y | $ANDROID_HOME/tools/bin/sdkmanager --update RUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'platforms;android-31' RUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'extras;android;m2repository' RUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk;23.1.7779620' RUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'platform-tools' RUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'build-tools;28.0.3' ENV PATH $PATH:$HOME/bin:$ANDROID_HOME/platform-tools ENV ANDROID_SDK_ROOT /build/android-sdk # We need some version of Go new enough to support the "embed" package # to run "go run tailscale.com/cmd/printdep" to figure out which Tailscale Go # version we need later, but otherwise this toolchain isn't used: RUN curl -L https://go.dev/dl/go1.17.5.linux-amd64.tar.gz | tar -C /usr/local -zxv RUN ln -s /usr/local/go/bin/go /usr/bin RUN mkdir -p $HOME/tailscale-android WORKDIR $HOME/tailscale-android # Preload Gradle COPY android/gradlew android/gradlew COPY android/gradle android/gradle RUN ./android/gradlew CMD /bin/bash ================================================ FILE: LICENSE ================================================ Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Tailscale Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ # Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. DEBUG_APK=tailscale-debug.apk RELEASE_AAB=tailscale-release.aab APPID=com.tailscale.ipn AAR=android/libs/ipn.aar KEYSTORE=tailscale.jks KEYSTORE_ALIAS=tailscale TAILSCALE_VERSION=$(shell ./version/tailscale-version.sh 200) OUR_VERSION=$(shell git describe --dirty --exclude "*" --always --abbrev=200) TAILSCALE_VERSION_ABBREV=$(shell ./version/tailscale-version.sh 11) OUR_VERSION_ABBREV=$(shell git describe --dirty --exclude "*" --always --abbrev=11) VERSION_LONG=$(TAILSCALE_VERSION_ABBREV)-g$(OUR_VERSION_ABBREV) # Extract the long version build.gradle's versionName and strip quotes. VERSIONNAME=$(patsubst "%",%,$(lastword $(shell grep versionName android/build.gradle))) # Extract the x.y.z part for the short version. VERSIONNAME_SHORT=$(shell echo $(VERSIONNAME) | cut -d - -f 1) TAILSCALE_COMMIT=$(shell echo $(TAILSCALE_VERSION) | cut -d - -f 2 | cut -d t -f 2) # Extract the version code from build.gradle. VERSIONCODE=$(lastword $(shell grep versionCode android/build.gradle)) VERSIONCODE_PLUSONE=$(shell expr $(VERSIONCODE) + 1) TOOLCHAINREV=$(shell go run tailscale.com/cmd/printdep --go) TOOLCHAINDIR=${HOME}/.cache/tailscale-android-go-$(TOOLCHAINREV) TOOLCHAINSUM=$(shell $(TOOLCHAINDIR)/go/bin/go version >/dev/null && echo "okay" || echo "bad") TOOLCHAINWANT=okay export PATH := $(TOOLCHAINDIR)/go/bin:$(PATH) export GOROOT := # Unset all: $(APK) tag_release: sed -i'.bak' 's/versionCode [[:digit:]]\+/versionCode $(VERSIONCODE_PLUSONE)/' android/build.gradle sed -i'.bak' 's/versionName .*/versionName "$(VERSION_LONG)"/' android/build.gradle git commit -sm "android: bump version code" android/build.gradle git tag -a "$(VERSION_LONG)" bumposs: toolchain GOPROXY=direct go get tailscale.com@main go mod tidy -compat=1.17 toolchain: ifneq ($(TOOLCHAINWANT),$(TOOLCHAINSUM)) @echo want: $(TOOLCHAINWANT) @echo got: $(TOOLCHAINSUM) rm -rf ${HOME}/.cache/tailscale-android-go-* mkdir -p $(TOOLCHAINDIR) curl --silent -L $(shell go run tailscale.com/cmd/printdep --go-url) | tar -C $(TOOLCHAINDIR) -zx endif $(DEBUG_APK): toolchain mkdir -p android/libs go run gioui.org/cmd/gogio -buildmode archive -target android -appid $(APPID) -tags novulkan -o $(AAR) github.com/tailscale/tailscale-android/cmd/tailscale (cd android && ./gradlew test assemblePlayDebug) mv android/build/outputs/apk/play/debug/android-play-debug.apk $@ rundebug: $(DEBUG_APK) adb install -r $(DEBUG_APK) adb shell am start -n com.tailscale.ipn/com.tailscale.ipn.IPNActivity # tailscale-fdroid.apk builds a non-Google Play SDK, without the Google bits. # This is effectively what the F-Droid build definition produces. # This is useful for testing on e.g. Amazon Fire Stick devices. tailscale-fdroid.apk: toolchain mkdir -p android/libs go run gioui.org/cmd/gogio -buildmode archive -target android -appid $(APPID) -tags novulkan -o $(AAR) github.com/tailscale/tailscale-android/cmd/tailscale (cd android && ./gradlew test assembleFdroidDebug) mv android/build/outputs/apk/fdroid/debug/android-fdroid-debug.apk $@ # This target is also used by the F-Droid builder. release_aar: toolchain release_aar: mkdir -p android/libs go run gioui.org/cmd/gogio -ldflags "-X tailscale.com/version.Long=$(VERSIONNAME) -X tailscale.com/version.Short=$(VERSIONNAME_SHORT) -X tailscale.com/version.GitCommit=$(TAILSCALE_COMMIT) -X tailscale.com/version.ExtraGitCommit=$(OUR_VERSION)" -buildmode archive -target android -appid $(APPID) -tags novulkan -o $(AAR) github.com/tailscale/tailscale-android/cmd/tailscale $(RELEASE_AAB): release_aar (cd android && ./gradlew test bundlePlayRelease) mv ./android/build/outputs/bundle/playRelease/android-play-release.aab $@ release: $(RELEASE_AAB) jarsigner -sigalg SHA256withRSA -digestalg SHA-256 -keystore $(KEYSTORE) $(RELEASE_AAB) $(KEYSTORE_ALIAS) install: $(DEBUG_APK) adb install -r $(DEBUG_APK) dockershell: docker build -t tailscale-android . docker run -v $(CURDIR):/build/tailscale-android -it --rm tailscale-android clean: rm -rf android/build $(RELEASE_AAB) $(DEBUG_APK) $(AAR) .PHONY: all clean install $(DEBUG_APK) $(RELEASE_AAB) release_aar release bump_version dockershell ================================================ FILE: PATENTS ================================================ Additional IP Rights Grant (Patents) "This implementation" means the copyrightable works distributed by Tailscale Inc. as part of the Tailscale project. Tailscale Inc. hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, transfer and otherwise run, modify and propagate the contents of this implementation of Tailscale, where such license applies only to those patent claims, both currently owned or controlled by Tailscale Inc. and acquired in the future, licensable by Tailscale Inc. that are necessarily infringed by this implementation of Tailscale. This grant does not include claims that would be infringed only as a consequence of further modification of this implementation. If you or your agent or exclusive licensee institute or order or agree to the institution of patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that this implementation of Tailscale or any code incorporated within this implementation of Tailscale constitutes direct or contributory patent infringement, or inducement of patent infringement, then any patent rights granted to you under this License for this implementation of Tailscale shall terminate as of the date such litigation is filed. ================================================ FILE: README.md ================================================ # Tailscale Android Client https://tailscale.com Private WireGuard® networks made easy ## Overview This repository contains the open source Tailscale Android client. ## Using [Get it on F-Droid](https://f-droid.org/packages/com.tailscale.ipn/) [Get it on Google Play](https://play.google.com/store/apps/details?id=com.tailscale.ipn) ## Building [Go](https://golang.org), the [Android SDK](https://developer.android.com/studio/releases/platform-tools), the [Android NDK](https://developer.android.com/ndk) are required. ```sh $ make tailscale-debug.apk $ adb install -r tailscale-debug.apk ``` The `dockershell` target builds a container with the necessary dependencies and runs a shell inside it. ```sh $ make dockershell # make tailscale-debug.apk ``` If you have Nix 2.4 or later installed, a Nix development environment can be set up with ```sh $ alias nix='nix --extra-experimental-features "nix-command flakes"' $ nix develop ``` Use `make tag_release` to bump the Android version code, update the version name, and tag the current commit. We only guarantee to support the latest Go release and any Go beta or release candidate builds (currently Go 1.14) in module mode. It might work in earlier Go versions or in GOPATH mode, but we're making no effort to keep those working. ## Google Sign-In Google Sign-In support relies on configuring a [Google API Console project](https://developers.google.com/identity/sign-in/android/start-integrating) with the app identifier and [signing key hashes](https://developers.google.com/android/guides/client-auth). The official release uses the app identifier `com.tailscale.ipn`; custom builds should use a different identifier. ## Running in the Android emulator By default, the android emulator uses an older version of OpenGL ES, which results in a black screen when opening the Tailscale app. To fix this, with the emulator running: - Open the three-dots menu to access emulator settings - To to `Settings > Advanced` - Set "OpenGL ES API level" to "Renderer maximum (up to OpenGL ES 3.1)" - Close the emulator. - In Android Studio's emulator view (that lists all your emulated devices), hit the down arrow by the virtual device and select "Cold boot now" to restart the emulator from scratch. The Tailscale app should now render correctly. Additionally, there seems to be a bug that prevents using the system-level Google sign-in option (the one that pops up a system-level UI to select your Google account). You can work around this by selecting "Other" at the sign-in screen, and then selecting Google from the next screen. ## Developing on a Fire Stick TV On the Fire Stick: * Settings > My Fire TV > Developer Options > ADB Debugging > ON Then some useful commands: ``` adb connect 10.2.200.213:5555 adb install -r tailscale-fdroid.apk adb shell am start -n com.tailscale.ipn/com.tailscale.ipn.IPNActivity adb shell pm uninstall com.tailscale.ipn ``` ## Building on macOS To build from the CLI on macOS: 1. Install Android Studio 2. In Android Studio's home screen: "More Actions" > "SDK Manager", install NDK. 3. You can now close Android Studio, unless you want it to create virtual devices ("More Actions" > "Virtual Device Manager"). 4. Then, from CLI: 5. `export JAVA_HOME='/Applications/Android Studio.app/Contents/jre/Contents/Home'` 6. `export ANDROID_SDK_ROOT=$HOME/Library/Android/sdk` 7. `make tailscale-fdroid.apk`, etc ## Bugs Please file any issues about this code or the hosted service on [the tailscale issue tracker](https://github.com/tailscale/tailscale/issues). ## Contributing `under_construction.gif` PRs welcome, but we are still working out our contribution process and tooling. We require [Developer Certificate of Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) `Signed-off-by` lines in commits. ## About Us We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, from Tailscale Inc. You can learn more about us from [our website](https://tailscale.com). WireGuard is a registered trademark of Jason A. Donenfeld. ================================================ FILE: android/build.gradle ================================================ buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.2.0' } } allprojects { repositories { google() jcenter() flatDir { dirs 'libs' } } } apply plugin: 'com.android.application' android { ndkVersion "23.1.7779620" compileSdkVersion 30 defaultConfig { minSdkVersion 22 targetSdkVersion 30 versionCode 114 versionName "1.29.0-t3c892d106-g42f688f1292" } compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } 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. } } } dependencies { implementation "androidx.core:core:1.2.0" implementation "androidx.browser:browser:1.2.0" implementation "androidx.security:security-crypto:1.1.0-alpha03" implementation ':ipn@aar' testCompile "junit:junit:4.12" // Non-free dependencies. playImplementation 'com.google.android.gms:play-services-auth:18.0.0' } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=3239b5ed86c3838a37d983ac100573f64c1f3fd8e1eb6c89fa5f9529b5ec091d distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ android.useAndroidX=true ================================================ FILE: android/gradlew ================================================ #!/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='"-Xmx64m" "-Xms64m"' # 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" "$@" ================================================ FILE: android/gradlew.bat ================================================ @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="-Xmx64m" "-Xms64m" @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 ================================================ FILE: android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/src/main/java/com/tailscale/ipn/App.java ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package 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 final static 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 final static Handler mainHandler = new Handler(Looper.getMainLooper()); public DnsConfig dns = new DnsConfig(this); public DnsConfig getDnsConfigObj() { return this.dns; } @Override public void onCreate() { super.onCreate(); // Load and initialize the Go library. Gio.init(this); registerNetworkCallback(); 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); } private void registerNetworkCallback() { ConnectivityManager cMgr = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE); cMgr.registerNetworkCallback(new NetworkRequest.Builder().build(), new ConnectivityManager.NetworkCallback() { private void reportConnectivityChange() { NetworkInfo active = cMgr.getActiveNetworkInfo(); // https://developer.android.com/training/monitoring-device-state/connectivity-status-type boolean isConnected = active != null && active.isConnectedOrConnecting(); onConnectivityChanged(isConnected); } @Override public void onLost(Network network) { super.onLost(network); this.reportConnectivityChange(); } @Override public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { super.onLinkPropertiesChanged(network, linkProperties); this.reportConnectivityChange(); } }); } public void startVPN() { Intent intent = new Intent(this, IPNService.class); intent.setAction(IPNService.ACTION_CONNECT); startService(intent); } public void stopVPN() { Intent intent = new Intent(this, IPNService.class); intent.setAction(IPNService.ACTION_DISCONNECT); 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 ); } void setTileReady(boolean ready) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { return; } QuickToggleService.setReady(this, ready); } 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 nameFromSystemBluetooth = Settings.System.getString(getContentResolver(), "bluetooth_name"); String nameFromSecureBluetooth = Settings.Secure.getString(getContentResolver(), "bluetooth_name"); String nameFromSystemDevice = Settings.Secure.getString(getContentResolver(), "device_name"); if (!isEmpty(nameFromSystemBluetooth)) return nameFromSystemBluetooth; if (!isEmpty(nameFromSecureBluetooth)) return nameFromSecureBluetooth; 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 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()); } private 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 onConnectivityChanged(boolean connected); 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; } } ================================================ FILE: android/src/main/java/com/tailscale/ipn/DnsConfig.java ================================================ // Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package com.tailscale.ipn; import android.content.Context; import android.net.ConnectivityManager; import android.net.DhcpInfo; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.wifi.WifiManager; import java.lang.reflect.Method; import java.net.InetAddress; import java.util.ArrayList; import java.util.List; import java.util.Locale; // 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. // // --------------------- Android 7 and later ----------------------------------------- // // ## getDnsConfigFromLinkProperties // Android provides a getAllNetworks interface in the ConnectivityManager. We walk through // each interface to pick the most appropriate one. // - If there is an Ethernet interface active we use that. // - If Wi-Fi is active we use that. // - If LTE is active we use that. // - We never use a VPN's DNS servers. That VPN is likely us. Even if not us, Android // only allows one VPN at a time so a different VPN's DNS servers won't be available // once Tailscale comes up. // // getAllNetworks() is used as the sole mechanism for retrieving the DNS config with // Android 7 and later. // // --------------------- Releases older than Android 7 ------------------------------- // // We support Tailscale back to Android 5. Android versions 5 and 6 supply a getAllNetworks() // implementation but it always returns an empty list. // // ## getDnsConfigFromLinkProperties with getActiveNetwork // ConnectivityManager also supports a getActiveNetwork() routine, which Android 5 and 6 do // return a value for. If Tailscale isn't up yet and we can get the Wi-Fi/LTE/etc DNS // config using getActiveNetwork(), we use that. // // Once Tailscale is up, getActiveNetwork() returns tailscale0 with DNS server 100.100.100.100 // and that isn't useful. So we try two other mechanisms: // // ## getDnsServersFromSystemProperties // Android versions prior to 8 let us retrieve the actual system DNS servers from properties. // Later Android versions removed the properties and only return an empty string. // // We check the net.dns1 - net.dns4 DNS servers. If Tailscale is up the DNS server will be // 100.100.100.100, which isn't useful, but if we get something different we'll use that. // // getDnsServersFromSystemProperties can only retrieve the IPv4 or IPv6 addresses of the // configured DNS servers. We also want to know the DNS Search Domains configured, but // we have no way to retrieve this using these interfaces. We return an empty list of // search domains. Sorry. // // ## getDnsServersFromNetworkInfo // ConnectivityManager supports an older API called getActiveNetworkInfo to return the // active network interface. It doesn't handle VPNs, so the interface will always be Wi-Fi // or Cellular even if Tailscale is up. // // For Wi-Fi interfaces we retrieve the DHCP response from the WifiManager. For Cellular // interfaces we check for properties populated by most of the radio drivers. // // getDnsServersFromNetworkInfo does not have a way to retrieve the DNS Search Domains, // so we return an empty list. Additionally, these interfaces are so old that they only // support IPv4. We can't retrieve IPv6 DNS server addresses this way. public class DnsConfig { private Context ctx; public DnsConfig(Context ctx) { this.ctx = ctx; } // 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() { String s = getDnsConfigFromLinkProperties(); if (!s.trim().isEmpty()) { return s; } if (android.os.Build.VERSION.SDK_INT >= 23) { // If ConnectivityManager.getAllNetworks() works, it is the // authoritative mechanism and we rely on it. The other methods // which follow involve more compromises. return ""; } s = getDnsServersFromSystemProperties(); if (!s.trim().isEmpty()) { return s; } return getDnsServersFromNetworkInfo(); } // getDnsConfigFromLinkProperties finds the DNS servers for each Network interface // returned by ConnectivityManager getAllNetworks().LinkProperties, and return the // one that (heuristically) would be the primary DNS servers. // // on a Nexus 4 with Android 5.1 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1 // on a Nexus 7 with Android 6.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1 // on a Pixel 3a with Android 12.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1\nlocaldomain // on a Pixel 3a with Android 12.0 on LTE: fd00:976a::9 fd00:976a::10 // // One odd behavior noted on Pixel3a with Android 12: // With Wi-Fi already connected, starting Tailscale returned DNS servers 2602:248:7b4a:ff60::1 10.1.10.1 // Turning off Wi-Fi and connecting LTE returned DNS servers fd00:976a::9 fd00:976a::10. // Turning Wi-Fi back on return DNS servers: 10.1.10.1. The IPv6 DNS server is gone. // This appears to be the ConnectivityManager behavior, not something we are doing. // // This implementation can work through Android 12 (SDK 30). In SDK 31 the // getAllNetworks() method is deprecated and we'll need to implement a // android.net.ConnectivityManager.NetworkCallback instead to monitor // link changes and track which DNS server to use. String getDnsConfigFromLinkProperties() { ConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); if (cMgr == null) { return ""; } Network[] networks = cMgr.getAllNetworks(); if (networks == null) { // Android 6 and before often returns an empty list, but we // can try again with just the active network. // // Once Tailscale is connected, the active network will be Tailscale // which will have 100.100.100.100 for its DNS server. We reject // TYPE_VPN in getPreferabilityForNetwork, so it won't be returned. Network active = cMgr.getActiveNetwork(); if (active == null) { return ""; } networks = new Network[]{active}; } // getPreferabilityForNetwork returns an index into dnsConfigs from 0-3. String[] dnsConfigs = new String[]{"", "", "", ""}; for (Network network : networks) { int idx = getPreferabilityForNetwork(cMgr, network); if ((idx < 0) || (idx > 3)) { continue; } LinkProperties linkProp = cMgr.getLinkProperties(network); NetworkCapabilities nc = cMgr.getNetworkCapabilities(network); List dnsList = linkProp.getDnsServers(); StringBuilder sb = new StringBuilder(""); for (InetAddress ip : dnsList) { sb.append(ip.getHostAddress() + " "); } String d = linkProp.getDomains(); if (d != null) { sb.append("\n"); sb.append(d); } dnsConfigs[idx] = sb.toString(); } // return the lowest index DNS config which exists. If an Ethernet config // was found, return it. Otherwise if Wi-fi was found, return it. Etc. for (String s : dnsConfigs) { if (!s.trim().isEmpty()) { return s; } } return ""; } // getDnsServersFromSystemProperties returns DNS servers found in system properties. // On Android versions prior to Android 8, we can directly query the DNS // servers the system is using. More recent Android releases return empty strings. // // Once Tailscale is connected these properties will return 100.100.100.100, which we // suppress. // // on a Nexus 4 with Android 5.1 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1 // on a Nexus 7 with Android 6.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1 // on a Pixel 3a with Android 12.0 on wifi: // on a Pixel 3a with Android 12.0 on LTE: // // The list of DNS search domains does not appear to be available in system properties. String getDnsServersFromSystemProperties() { try { Class SystemProperties = Class.forName("android.os.SystemProperties"); Method method = SystemProperties.getMethod("get", String.class); List servers = new ArrayList(); for (String name : new String[]{"net.dns1", "net.dns2", "net.dns3", "net.dns4"}) { String value = (String) method.invoke(null, name); if (value != null && !value.isEmpty() && !value.equals("100.100.100.100") && !servers.contains(value)) { servers.add(value); } } return String.join(" ", servers); } catch (Exception e) { return ""; } } public String intToInetString(int hostAddress) { return String.format(java.util.Locale.ROOT, "%d.%d.%d.%d", (0xff & hostAddress), (0xff & (hostAddress >> 8)), (0xff & (hostAddress >> 16)), (0xff & (hostAddress >> 24))); } // getDnsServersFromNetworkInfo retrieves DNS servers using ConnectivityManager // getActiveNetworkInfo() plus interface-specific mechanisms to retrieve the DNS servers. // Only IPv4 DNS servers are supported by this mechanism, neither the WifiManager nor the // interface-specific dns properties appear to populate IPv6 DNS server addresses. // // on a Nexus 4 with Android 5.1 on wifi: 10.1.10.1 // on a Nexus 7 with Android 6.0 on wifi: 10.1.10.1 // on a Pixel-3a with Android 12.0 on wifi: 10.1.10.1 // on a Pixel-3a with Android 12.0 on LTE: // // The list of DNS search domains is not available in this way. String getDnsServersFromNetworkInfo() { ConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); if (cMgr == null) { return ""; } NetworkInfo info = cMgr.getActiveNetworkInfo(); if (info == null) { return ""; } Class SystemProperties; Method method; try { SystemProperties = Class.forName("android.os.SystemProperties"); method = SystemProperties.getMethod("get", String.class); } catch (Exception e) { return ""; } List servers = new ArrayList(); switch(info.getType()) { case ConnectivityManager.TYPE_WIFI: case ConnectivityManager.TYPE_WIMAX: for (String name : new String[]{ "net.wifi0.dns1", "net.wifi0.dns2", "net.wifi0.dns3", "net.wifi0.dns4", "net.wlan0.dns1", "net.wlan0.dns2", "net.wlan0.dns3", "net.wlan0.dns4", "net.eth0.dns1", "net.eth0.dns2", "net.eth0.dns3", "net.eth0.dns4", "dhcp.wlan0.dns1", "dhcp.wlan0.dns2", "dhcp.wlan0.dns3", "dhcp.wlan0.dns4", "dhcp.tiwlan0.dns1", "dhcp.tiwlan0.dns2", "dhcp.tiwlan0.dns3", "dhcp.tiwlan0.dns4"}) { try { String value = (String) method.invoke(null, name); if (value != null && !value.isEmpty() && !servers.contains(value)) { servers.add(value); } } catch (Exception e) { continue; } } WifiManager wMgr = (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE); if (wMgr != null) { DhcpInfo dhcp = wMgr.getDhcpInfo(); if (dhcp.dns1 != 0) { String value = intToInetString(dhcp.dns1); if (value != null && !value.isEmpty() && !servers.contains(value)) { servers.add(value); } } if (dhcp.dns2 != 0) { String value = intToInetString(dhcp.dns2); if (value != null && !value.isEmpty() && !servers.contains(value)) { servers.add(value); } } } return String.join(" ", servers); case ConnectivityManager.TYPE_MOBILE: case ConnectivityManager.TYPE_MOBILE_HIPRI: for (String name : new String[]{ "net.rmnet0.dns1", "net.rmnet0.dns2", "net.rmnet0.dns3", "net.rmnet0.dns4", "net.rmnet1.dns1", "net.rmnet1.dns2", "net.rmnet1.dns3", "net.rmnet1.dns4", "net.rmnet2.dns1", "net.rmnet2.dns2", "net.rmnet2.dns3", "net.rmnet2.dns4", "net.rmnet3.dns1", "net.rmnet3.dns2", "net.rmnet3.dns3", "net.rmnet3.dns4", "net.rmnet4.dns1", "net.rmnet4.dns2", "net.rmnet4.dns3", "net.rmnet4.dns4", "net.rmnet5.dns1", "net.rmnet5.dns2", "net.rmnet5.dns3", "net.rmnet5.dns4", "net.rmnet6.dns1", "net.rmnet6.dns2", "net.rmnet6.dns3", "net.rmnet6.dns4", "net.rmnet7.dns1", "net.rmnet7.dns2", "net.rmnet7.dns3", "net.rmnet7.dns4", "net.pdp0.dns1", "net.pdp0.dns2", "net.pdp0.dns3", "net.pdp0.dns4", "net.pdpbr0.dns1", "net.pdpbr0.dns2", "net.pdpbr0.dns3", "net.pdpbr0.dns4"}) { try { String value = (String) method.invoke(null, name); if (value != null && !value.isEmpty() && !servers.contains(value)) { servers.add(value); } } catch (Exception e) { continue; } } } return ""; } // getPreferabilityForNetwork is a utility routine which implements a priority for // different types of network transport, used in a heuristic to pick DNS servers to use. int getPreferabilityForNetwork(ConnectivityManager cMgr, Network network) { NetworkCapabilities nc = cMgr.getNetworkCapabilities(network); if (nc == null) { return -1; } if (nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { // tun0 has both VPN and WIFI set, have to check VPN first and return. return -1; } if (nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { return 0; } else if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { return 1; } else if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { return 2; } else { return 3; } } } ================================================ FILE: android/src/main/java/com/tailscale/ipn/IPNActivity.java ================================================ // Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package com.tailscale.ipn; import android.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; 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(); } } } @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(); } } ================================================ FILE: android/src/main/java/com/tailscale/ipn/IPNService.java ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package 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 org.gioui.GioActivity; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; public class IPNService extends VpnService { public static final String ACTION_CONNECT = "com.tailscale.ipn.CONNECT"; public static final String ACTION_DISCONNECT = "com.tailscale.ipn.DISCONNECT"; @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) { close(); return START_NOT_STICKY; } 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); } 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"); 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 connect(); private native void disconnect(); } ================================================ FILE: android/src/main/java/com/tailscale/ipn/Peer.java ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package 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); } ================================================ FILE: android/src/main/java/com/tailscale/ipn/QuickToggleService.java ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package com.tailscale.ipn; import android.content.Context; import android.content.ComponentName; import android.content.Intent; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicBoolean; 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 static native void onTileClick(); } ================================================ FILE: android/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: android/src/main/res/drawable/ic_tile.xml ================================================ ================================================ FILE: android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: android/src/main/res/values/ic_launcher_background.xml ================================================ #1F2125 ================================================ FILE: android/src/main/res/values/strings.xml ================================================ Tailscale Tailscale ================================================ FILE: android/src/play/java/com/tailscale/ipn/Google.java ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package 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(); } } ================================================ FILE: android/src/test/java/com/tailscale/ipn/DnsConfigTest.java ================================================ import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; import com.tailscale.ipn.DnsConfig; public class DnsConfigTest { DnsConfig dns; @Before public void setup() { dns = new DnsConfig(null); } @Test public void dnsConfig_intToInetStringTest() { assertEquals(dns.intToInetString(0x0101a8c0), "192.168.1.1"); assertEquals(dns.intToInetString(0x04030201), "1.2.3.4"); assertEquals(dns.intToInetString(0), "0.0.0.0"); } } ================================================ FILE: cmd/tailscale/backend.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "errors" "fmt" "log" "net/http" "path/filepath" "reflect" "strings" "time" "github.com/tailscale/tailscale-android/jni" "golang.org/x/sys/unix" "golang.zx2c4.com/wireguard/tun" "inet.af/netaddr" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" "tailscale.com/logpolicy" "tailscale.com/logtail" "tailscale.com/logtail/filch" "tailscale.com/net/dns" "tailscale.com/net/tsdial" "tailscale.com/smallzstd" "tailscale.com/types/logger" "tailscale.com/util/dnsname" "tailscale.com/wgengine" "tailscale.com/wgengine/netstack" "tailscale.com/wgengine/router" ) type backend struct { engine wgengine.Engine backend *ipnlocal.LocalBackend devices *multiTUN settings settingsFunc lastCfg *router.Config lastDNSCfg *dns.OSConfig logIDPublic string // 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" ) 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 = []netaddr.IP{netaddr.MustParseIP("8.8.8.8"), netaddr.MustParseIP("8.8.4.4"), netaddr.MustParseIP("2001:4860:4860::8888"), netaddr.MustParseIP("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") func newBackend(dataDir string, jvm *jni.JVM, appCtx jni.Object, store *stateStore, settings settingsFunc) (*backend, error) { logf := logger.RusagePrefixLog(log.Printf) b := &backend{ jvm: jvm, devices: newTUNDevices(), settings: settings, appCtx: appCtx, } var logID logtail.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 := logtail.NewPrivateID() if err == nil { logID = newLogID enc, err := newLogID.MarshalText() if err == nil { store.write(logPrefKey, enc) } } } else { logID.UnmarshalText([]byte(storedLogID)) } b.SetupLogs(dataDir, logID) 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, }) if err != nil { return nil, fmt.Errorf("runBackend: NewUserspaceEngine: %v", err) } b.logIDPublic = logID.Public().String() tunDev, magicConn, dnsMgr, ok := engine.(wgengine.InternalsGetter).GetInternals() if !ok { return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", engine) } ns, err := netstack.Create(logf, tunDev, engine, magicConn, dialer, dnsMgr) if err != nil { return nil, fmt.Errorf("netstack.Create: %w", err) } ns.ProcessLocalIPs = false // let Android kernel handle it; VpnBuilder sets this up ns.ProcessSubnets = true // for Android-being-an-exit-node support lb, err := ipnlocal.NewLocalBackend(logf, b.logIDPublic, store, dialer, engine, 0) if err != nil { engine.Close() return nil, fmt.Errorf("runBackend: NewLocalBackend: %v", err) } ns.SetLocalBackend(lb) if err := ns.Start(); err != nil { return nil, fmt.Errorf("startNetstack: %w", err) } b.engine = engine b.backend = lb return b, nil } func (b *backend) Start(notify func(n ipn.Notify)) error { b.backend.SetNotifyCallback(notify) return b.backend.Start(ipn.Options{ StateKey: "ipn-android", }) } func (b *backend) LinkChange() { if b.engine != nil { b.engine.LinkChange(false) } } 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.IP().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.IP().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 { 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 logtail.PrivateID) { logcfg := logtail.Config{ Collection: "tailnode.log.tailscale.io", PrivateID: logID, Stderr: log.Writer(), NewZstdEncoder: func() logtail.Encoder { w, err := smallzstd.NewEncoder(nil) if err != nil { panic(err) } return w }, HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost)}, } drainCh := make(chan struct{}) logcfg.DrainLogs = drainCh go func() { // Upload logs infrequently. Interval chosen arbitrarily. // The objective is to reduce phone power use. t := time.NewTicker(2 * time.Minute) for range t.C { select { case drainCh <- struct{}{}: default: } } }() filchOpts := filch.Options{ ReplaceStderr: true, } var filchErr error if logDir != "" { logPath := filepath.Join(logDir, "ipn.log.") logcfg.Buffer, filchErr = filch.New(logPath, filchOpts) } logf := logger.RusagePrefixLog(log.Printf) tlog := logtail.NewLogger(logcfg, logf) log.SetFlags(0) log.SetOutput(tlog) 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) } } // We log the result of each of the DNS configuration discovery mechanisms, as we're // expecting a long tail of obscure Android devices with interesting behavior. func (b *backend) logDNSConfigMechanisms() { 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 JNI: %v", err) } dnsCls := jni.GetObjectClass(env, dns) for _, impl := range []string{"getDnsConfigFromLinkProperties", "getDnsServersFromSystemProperties", "getDnsServersFromNetworkInfo"} { m = jni.GetMethodID(env, dnsCls, impl, "()Ljava/lang/String;") n, err := jni.CallObjectMethod(env, dns, m) baseConfig := jni.GoString(env, jni.String(n)) if err != nil { log.Printf("%s JNI: %v", impl, err) } else { oneLine := strings.Replace(baseConfig, "\n", ";", -1) log.Printf("%s: %s", impl, oneLine) } } return nil }) if err != nil { log.Printf("logDNSConfigMechanisms: %v", err) } } 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() (dns.OSConfig, error) { b.logDNSConfigMechanisms() 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 := netaddr.ParseIP(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 } ================================================ FILE: cmd/tailscale/callbacks.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main // JNI implementations of Java native callback methods. import ( "unsafe" "github.com/tailscale/tailscale-android/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) // onConnect receives global IPNService references when // a VPN connection is requested. onConnect = make(chan jni.Object) // onDisconnect receives global IPNService references when // disconnecting. onDisconnect = make(chan jni.Object) // onConnectivityChange is notified every time the network // conditions change. onConnectivityChange = make(chan bool, 1) // 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) ) 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_connect func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) { jenv := (*jni.Env)(unsafe.Pointer(env)) onConnect <- jni.NewGlobalRef(jenv, jni.Object(this)) } //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_App_onConnectivityChanged func Java_com_tailscale_ipn_App_onConnectivityChanged(env *C.JNIEnv, cls C.jclass, connected C.jboolean) { select { case <-onConnectivityChange: default: } onConnectivityChange <- connected == C.JNI_TRUE } //export Java_com_tailscale_ipn_QuickToggleService_onTileClick func Java_com_tailscale_ipn_QuickToggleService_onTileClick(env *C.JNIEnv, cls C.jclass) { requestBackend(ToggleEvent{}) } //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 } ================================================ FILE: cmd/tailscale/main.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "context" "crypto/rand" "crypto/sha1" "encoding/hex" "errors" "fmt" "io" "log" "mime" "net" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "sync/atomic" "time" "unsafe" "gioui.org/app" "gioui.org/io/system" "gioui.org/layout" "gioui.org/op" "inet.af/netaddr" "github.com/tailscale/tailscale-android/jni" "tailscale.com/client/tailscale/apitype" "tailscale.com/hostinfo" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" "tailscale.com/net/dns" "tailscale.com/net/interfaces" "tailscale.com/net/netns" "tailscale.com/paths" "tailscale.com/tailcfg" "tailscale.com/types/netmap" "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.Value // of string // 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{} } 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 } 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 } 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 } // UIEvent types. type ( ToggleEvent struct{} ReauthEvent struct{} BugEvent struct{} WebAuthEvent struct{} GoogleAuthEvent struct{} LogoutEvent struct{} BeExitNodeEvent bool ExitAllowLANEvent bool ) // 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), } err := 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 }) if err != nil { fatalErr(err) } a.store = newStateStore(a.jvm, a.appCtx) interfaces.RegisterInterfaceGetter(a.getInterfaces) go func() { if err := a.runBackend(); err != nil { fatalErr(err) } }() go func() { if err := a.runUI(); err != nil { fatalErr(err) } }() app.Main() } func (a *App) runBackend() 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) defer b.CloseTUNs() // 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 { first := state.Prefs == nil state.Prefs = p.Clone() state.updateExitNodes() if first { 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 { a.updateNotification(service, state.State) } if service != 0 { 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() } } 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 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 OAuth2Event: go b.backend.Login(e.Token) case ToggleEvent: state.Prefs.WantRunning = !state.Prefs.WantRunning go b.backend.SetPrefs(state.Prefs) case BeExitNodeEvent: state.Prefs.SetAdvertiseExitNode(bool(e)) go b.backend.SetPrefs(state.Prefs) case ExitAllowLANEvent: state.Prefs.ExitNodeAllowLANAccess = bool(e) go b.backend.SetPrefs(state.Prefs) case WebAuthEvent: if !signingIn { go b.backend.StartLoginInteractive() signingIn = true } case SetLoginServerEvent: state.Prefs.ControlURL = e.URL b.backend.SetPrefs(state.Prefs) // A hack to get around ipnlocal's inability to update // ControlURL after Start()... Can we re-init instead? os.Exit(0) case LogoutEvent: go b.backend.Logout() case ConnectEvent: state.Prefs.WantRunning = e.Enable go b.backend.SetPrefs(state.Prefs) case RouteAllEvent: state.Prefs.ExitNodeID = e.ID go b.backend.SetPrefs(state.Prefs) state.updateExitNodes() a.notify(state) } case s := <-onConnect: 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. }) }) service = s return nil }) a.updateNotification(service, state.State) 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 connected := <-onConnectivityChange: if state.LostInternet != !connected { log.Printf("LostInternet state change: %v -> %v", state.LostInternet, !connected) } state.LostInternet = !connected if b != nil { go b.LinkChange() } a.notify(state) 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() } } } } 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.Node if s.NetworkMap != nil { peers = s.NetworkMap.Peers } for _, p := range peers { canRoute := false for _, r := range p.AllowedIPs { if r == netaddr.MustParseIPPrefix("0.0.0.0/0") || r == netaddr.MustParseIPPrefix("::/0") { canRoute = true break } } myExit := p.StableID == exitID hasMyExit = hasMyExit || myExit exit := Peer{ Label: p.DisplayName(true), Online: canRoute, ID: p.StableID, } if myExit { s.Exit = exit if canRoute { s.ExitStatus = ExitOnline } } if canRoute || myExit { s.Exits = append(s.Exits, exit) } } sort.Slice(s.Exits, func(i, j int) bool { return s.Exits[i].Label < s.Exits[j].Label }) 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) error { var msg, title string switch state { case ipn.Starting: title, msg = "Connecting...", "" case ipn.Running: title, msg = "Connected", "" 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 err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil { return err } if activity != 0 { 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 *system.CommandEvent: if e.Type == system.CommandBack { if ui.onBack() { e.Cancel = true 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.Node 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 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 len(p.Addresses) > 0 { addr = p.Addresses[0].IP().String() } if !strings.Contains(host, q) && !strings.Contains(name, q) && !strings.Contains(addr, q) { continue } } users[p.User] = struct{}{} uiPeers = append(uiPeers, UIPeer{ Owner: p.User, Peer: p, }) } // 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: backendLogID, _ := a.logIDPublicAtomic.Load().(string) logMarker := fmt.Sprintf("BUG-%v-%v-%v", backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8)) log.Printf("user bugreport: %s", logMarker) w.WriteClipboard(logMarker) case BeExitNodeEvent: requestBackend(e) case ExitAllowLANEvent: requestBackend(e) case WebAuthEvent: 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: 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) } } } 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) } ================================================ FILE: cmd/tailscale/multitun.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "os" "golang.zx2c4.com/wireguard/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 flushes chan chan error 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 offset int reply chan<- ioReply } type ioReply struct { bytes 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), flushes: make(chan chan error), 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 f := <-d.flushes: var err error if len(devices) > 0 { dev := devices[len(devices)-1] err = dev.dev.Flush() } f <- err 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.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, offset int) (int, error) { r := make(chan ioReply) d.reads <- ioRequest{data, offset, r} rep := <-r return rep.bytes, rep.err } func (d *multiTUN) Write(data []byte, offset int) (int, error) { r := make(chan ioReply) d.writes <- ioRequest{data, offset, r} rep := <-r return rep.bytes, rep.err } func (d *multiTUN) Flush() error { r := make(chan error) d.flushes <- r return <-r } 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 } ================================================ FILE: cmd/tailscale/pprof.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build pprof // +build pprof package main import ( "net/http" _ "net/http/pprof" ) func init() { go func() { http.ListenAndServe(":6060", nil) }() } ================================================ FILE: cmd/tailscale/store.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "encoding/base64" "tailscale.com/ipn" "github.com/tailscale/tailscale-android/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 } ================================================ FILE: cmd/tailscale/tools.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build tools // +build tools package main import ( _ "gioui.org/cmd/gogio" ) ================================================ FILE: cmd/tailscale/ui.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bytes" "context" "fmt" "image" "image/color" "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" "inet.af/netaddr" "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn" "tailscale.com/tailcfg" _ "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 useLoginServer widget.Clickable copy widget.Clickable reauth widget.Clickable bug widget.Clickable beExit widget.Clickable exits 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 } 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 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 } return nil } 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.menu.show = !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.Addresses; len(addrs) > 0 { localAddr = addrs[0].IP().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, relaunch the app") events = append(events, SetLoginServerEvent{URL: text}) } 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.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 || state.backend.LostInternet { return D{} } return ui.layoutSignIn(gtx, &state.backend) case 5: if !state.backend.LostInternet { return D{} } return ui.layoutDisconnected(gtx) 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].IP().String() events = append(events, CopyEvent{Text: a}) ui.showCopied(gtx, a) } } return ui.layoutPeer(gtx, sysIns, p, userID, clk) } } }) }) ui.layoutExitNodeDialog(gtx, sysIns, state.backend.Exits) ui.layoutShareDialog(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) } paint.Fill(gtx.Ops, bg) return material.Clickable(gtx, &ui.openExitDialog, func(gtx C) D { gtx.Constraints.Min.X = gtx.Constraints.Max.X 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.Px(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 and restart") 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.Px(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.Px(unit.Dp(16)), } return material.Loader(ui.theme).Layout(gtx) }) }), ) } // layoutDisconnected lays out the "please connect to the internet" // message. func (ui *UI) layoutDisconnected(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Inset{Top: unit.Dp(8)}.Layout(gtx, func(gtx C) D { title := material.H6(ui.theme, "No internet connection") title.Alignment = text.Middle return title.Layout(gtx) }) }), layout.Rigid(func(gtx C) D { return layout.Inset{Top: unit.Dp(8)}.Layout(gtx, func(gtx C) D { msg := material.Body2(ui.theme, "Tailscale is paused while your device is offline. Please reconnect to the internet.") msg.Alignment = text.Middle return msg.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.Px(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: unit.Add(gtx.Metric, 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.menu.show = 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: unit.Add(gtx.Metric, sysIns.Top, unit.Dp(16)), Right: unit.Add(gtx.Metric, sysIns.Right, unit.Dp(16)), Bottom: unit.Add(gtx.Metric, sysIns.Bottom, unit.Dp(16)), Left: unit.Add(gtx.Metric, 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.Px(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.Px(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.Px(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.Px(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: unit.Add(gtx.Metric, sysIns.Top, unit.Dp(16)), Right: unit.Add(gtx.Metric, sysIns.Right, unit.Dp(16)), Bottom: unit.Add(gtx.Metric, sysIns.Bottom, unit.Dp(16)), Left: unit.Add(gtx.Metric, 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.Px(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} 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) }) }), ) }) }) }) } 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.Px(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.menu.show = false } layout.Inset{ Top: unit.Add(gtx.Metric, sysIns.Top, unit.Dp(2)), Right: unit.Add(gtx.Metric, 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 { items := []menuItem{ {title: "Use login server", btn: &menu.useLoginServer}, } return layoutMenu(ui.theme, gtx, items, func(gtx C) D { l := material.Caption(ui.theme, "Advanced settings") 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}) 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: unit.Add(gtx.Metric, sysIns.Left, unit.Dp(8)), Right: unit.Add(gtx.Metric, sysIns.Right, unit.Dp(8)), Bottom: unit.Add(gtx.Metric, 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) } // 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: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(16)), Left: unit.Max(gtx.Metric, 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.Inset{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 netaddr.IP // IP to show; first IPv4, or first IPv6 if no IPv4 for _, addr := range p.Peer.Addresses { if ip := addr.IP(); bestIP.IsZero() || 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: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(16)), Left: unit.Max(gtx.Metric, 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: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)), Left: unit.Max(gtx.Metric, 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: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)), Left: unit.Max(gtx.Metric, 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: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)), Left: unit.Max(gtx.Metric, 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 machine name...").Layout, ), ) }) }) }) }) } // drawLogo draws the Tailscale logo using vector operations. func drawLogo(ops *op.Ops, size int) { scale := float32(size) / 680 discDia := 170 * scale off := 172 * 1.5 * scale tx := op.Offset(f32.Pt(off, 0)) ty := op.Offset(f32.Pt(0, off)) defer op.Offset(f32.Point{}).Push(ops).Pop() // First row of discs. row := op.Offset(f32.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(f32.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(f32.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.Value) layout.Dimensions { img.Add(gtx.Ops) sz := img.Size() aspect := float32(sz.Y) / float32(sz.X) w := gtx.Px(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) { r2 := radius * .5 dr := f32.Rectangle{Max: f32.Pt(radius, radius)} defer clip.RRect{ Rect: dr, 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.Value } 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.Px(b.CornerRadius); r > 0 { rr := float32(r) defer clip.RRect{ Rect: f32.Rectangle{Max: f32.Point{ X: float32(sz.X), Y: float32(sz.Y), }}, NE: rr, NW: rr, SE: rr, SW: rr, }.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.` ================================================ FILE: flake.nix ================================================ { description = "Tailscale build environment"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs"; android.url = "github:tadfisher/android-nixpkgs"; android.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, android }: let supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system); in { devShells = forAllSystems (system: let pkgs = import nixpkgs { inherit system; }; android-sdk = android.sdk.${system} (sdkPkgs: with sdkPkgs; [ build-tools-30-0-2 cmdline-tools-latest platform-tools platforms-android-31 platforms-android-30 ndk-23-1-7779620 patcher-v4 ]); in { default = (with pkgs; buildFHSUserEnv { name = "tailscale"; profile = '' export ANDROID_SDK_ROOT="${android-sdk}/share/android-sdk" export JAVA_HOME="${jdk8.home}" ''; targetPkgs = pkgs: with pkgs; [ android-sdk jdk8 clang ] ++ (if stdenv.isLinux then [ vulkan-headers libxkbcommon wayland xorg.libX11 xorg.libXcursor xorg.libXfixes libGL pkgconfig ] else [ ]); }).env; } ); }; } ================================================ FILE: go.mod ================================================ module github.com/tailscale/tailscale-android go 1.18 require ( eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 gioui.org v0.0.0-20220331105829-a1b5ff059c07 gioui.org/cmd v0.0.0-20210925100615-41f3a7e74ee6 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 inet.af/netaddr v0.0.0-20220617031823-097006376321 tailscale.com v1.1.1-0.20220718172352-3c892d106c2e ) require ( 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-20210105120005-909beea2cc74 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/benoitkugler/textlayout v0.0.10 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/creack/pty v1.1.17 // indirect github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506 // indirect github.com/godbus/dbus/v5 v5.0.6 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e // indirect github.com/josharian/native v1.0.0 // indirect github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect github.com/klauspost/compress v1.15.4 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect github.com/mdlayher/genetlink v1.2.0 // indirect github.com/mdlayher/netlink v1.6.0 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect github.com/mdlayher/socket v0.2.3 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/u-root/u-root v0.8.0 // indirect github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 // indirect github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect golang.org/x/tools v0.1.11 // indirect golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect golang.zx2c4.com/wireguard/windows v0.4.10 // indirect gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445 // indirect nhooyr.io/websocket v1.8.7 // indirect ) ================================================ FILE: go.sum ================================================ 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/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o= gioui.org v0.0.0-20210910062418-d5d0a75a9bcb/go.mod h1:BTldRXnY5mrUrYZCdWyDwyMzyUzpfZN1cF4MMRrOt9w= gioui.org v0.0.0-20220331105829-a1b5ff059c07 h1:9BdOhoKqRiItOl87wZfQUL2YZNm+6Yyp1f1iEiJUh+U= gioui.org v0.0.0-20220331105829-a1b5ff059c07/go.mod h1:b8vBukexG6eYuXZa14asjLAWJ+JjbZ/ophEnS2FjYUg= 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/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= 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-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 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/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE= github.com/benoitkugler/textlayout v0.0.5/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8= github.com/benoitkugler/textlayout v0.0.10 h1:uIaQgH4pBFw1LQ0tPkfjgxo94WYcckzzQaB41L2X84w= github.com/benoitkugler/textlayout v0.0.10/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8= 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/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.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao= github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= 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/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= 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.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 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/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= 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.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 h1:1bjaB/5IIicfKpP4k0s30T2WEw//Kh00zULa8DQ0cxA= github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12/go.mod h1:kDhBRTA/i3H46PVdhqcw26TdGSIj42TOKNWKY+Kipnw= 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-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.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 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-20220112121102-58fe93c84506 h1:1TPz/Gn/MsXwJ6bEtI9wdkPcQYr2X3V9I+wz4wPYUdY= github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506/go.mod h1:R0mlTNeyszZ/tKQhbZA7SRGjx+OHsmNzgN2jTV7yZcs= 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.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/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/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/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= 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-20211209223715-7d93572ebe8e h1:IQpunlq7T+NiJJMO7ODYV2YWBiv/KnObR3gofX0mWOo= github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo= github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs= github.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291 h1:0J2ntV09uHLUHC79Z3YKJX2EnfOKL2QkMuHabu4L8JM= github.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291/go.mod h1:J7jazXS6RFR/oZT8XdfdD2KQ1bl56ukeE1qt4w8UQaI= github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6kZr2O7PPdT+RkbSmmOponA8i/1DuGHe8BRsM= github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY= 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/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/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.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ= github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= 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-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 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/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= github.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60/go.mod h1:aYbhishWc4Ai3I2U4Gaa2n3kHWSwzme6EsG/46HRQbE= github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU= github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8= github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys= github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q= github.com/mdlayher/netlink v1.4.2/go.mod h1:13VaingaArGUTUxFLf/iEovKxXji32JAtF858jZYEug= github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0= github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697 h1:PBb7ld5cQGfxHF2pKvb/ydtuPwdRaltGI4e0QSCuiNI= github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4= github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc= github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g= github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g= github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI= github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 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/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= 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/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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w= 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/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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs= github.com/tailscale/golang-x-crypto v0.0.0-20220326011347-d690bbfb6b5f h1:SO0bJlfWstNuolA3zjWDcLq0mjLfIw6RWEImAPxCkSU= github.com/tailscale/golang-x-crypto v0.0.0-20220326011347-d690bbfb6b5f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU= github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf h1:+DSoknr7gaiW2LlViX6+ko8TBdxTLkvOBbIWQtYyMaE= github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU= github.com/tailscale/golang-x-crypto v0.0.0-20220420170900-3a580d9e7b34 h1:ibRgOygS0bLOht+rOfaTuTvHiwmqeteG+rlFYs18aD8= github.com/tailscale/golang-x-crypto v0.0.0-20220420170900-3a580d9e7b34/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU= github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f h1:3CuODoSnBXS+ZkQlGakDqtX1o2RteR1870yF+dS61PY= github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU= github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs= github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= 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/u-root v0.8.0 h1:jqP7uPC2+0eRszYTrmdZ6UDyO1Dbuy0rpMo+BnPZ9cY= github.com/u-root/u-root v0.8.0/go.mod h1:But1FHzS4Ua4ywx6kZOaRzZTucUKIDKOPOLEKOckQ68= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 h1:XMAtQHwKjWHIRwg+8Nj/rzUomQY1q6cM3ncA0wP8GU4= github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 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.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So= github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4= go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA= go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/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.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s= golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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 h1:IlrJD2AM5p8JhN/wVny9jt6gJ9hut2VALhSeZ3SYluk= golang.org/x/exp v0.0.0-20210722180016-6781d3edade3/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb h1:fP6C8Xutcp5AlakmT/SkQot0pMicROAsEX7OfNPuG10= 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-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 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/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.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20190419010253-1f3472d942ba/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-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220516155154-20f960328961 h1:+W/iTMPG0EL7aW+/atntZwZrvSRIj3m3yX414dSULUU= golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/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-20190606122018-79a91cf218c4/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/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-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1 h1:Z3vE1sGlC7qiyFJkkDcZms8Y3+yV8+W7HmDSmuf71tM= golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= 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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8= golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961 h1:oIXcKhP1Ge6cRqdpQuldl0hf4mjIsNaXojabghlHuTs= golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 h1:vDy//hdR+GnROE3OdYbQKt9rdtNdHkDtONvpRwmls/0= golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= golang.zx2c4.com/wireguard/windows v0.4.10 h1:HmjzJnb+G4NCdX+sfjsQlsxGPuYaThxRbZUZFLyR0/s= golang.zx2c4.com/wireguard/windows v0.4.10/go.mod h1:v7w/8FC48tTBm1IzScDVPEEb0/GjLta+T0ybpP9UWRg= 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= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gvisor.dev/gvisor v0.0.0-20220318082524-536b85ae1a6a h1:sQAuNyyy59GRxS8npo8nyOr5yM46gB7QzVFiq6yvHdg= gvisor.dev/gvisor v0.0.0-20220318082524-536b85ae1a6a/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI= gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445 h1:pLNQCtMzh4O6rdhoUeWHuutt4yMft+B9Cgw/bezWchE= gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI= 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= honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.3.0-0.dev.0.20220306074811-23e1086441d2 h1:utiSabORbG/JeX7MlmKMdmsjwom2+v8zmdb6SoBe4UY= honnef.co/go/tools v0.4.0-0.dev.0.20220404092545-59d7a2877f83 h1:lZ9GIYaU+o5+X6ST702I/Ntyq9Y2oIMZ42rBQpem64A= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw= inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8= 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.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= tailscale.com v1.1.1-0.20220327044527-7c7f37342fa7 h1:NH+oDzd3mnPQWiOqLyBZTvNj9PFo1p2NAJ8G0aCMiIo= tailscale.com v1.1.1-0.20220327044527-7c7f37342fa7/go.mod h1:ptcVi35GenDhRsw+7hLsIGSmY0W2lsZGBEODHS301tQ= tailscale.com v1.1.1-0.20220330175050-9f604f2bd3b4 h1:snGTFNSURzgRkPsEBZMAZcmkHsQWYBXELIHcnslLTk4= tailscale.com v1.1.1-0.20220330175050-9f604f2bd3b4/go.mod h1:JpNVbOb4vjyi/Ny9vgc8A0M8d6JQHC3RhyXS+6DOIhQ= tailscale.com v1.1.1-0.20220416053651-c591c9165379 h1:GKmD3oVdtPKERevIiqCuVZHVMwpdcdfzOq7RkJwJ3ys= tailscale.com v1.1.1-0.20220416053651-c591c9165379/go.mod h1:8UcUkPq+JvAmq/GYyeNRP/1aQwly/33dQu6IEU+lh/A= tailscale.com v1.1.1-0.20220419050352-c13be0c5090c h1:jN2RyioI34KnqpjobSeiH7V0drz4YU4v7mxADoUqz+I= tailscale.com v1.1.1-0.20220419050352-c13be0c5090c/go.mod h1:8UcUkPq+JvAmq/GYyeNRP/1aQwly/33dQu6IEU+lh/A= tailscale.com v1.1.1-0.20220421004349-13f75b9667c0 h1:zs3cQ6D/PFUuGq+cRi1u0S4+3Sbz5A/Id5hzlmQtbbg= tailscale.com v1.1.1-0.20220421004349-13f75b9667c0/go.mod h1:YsCSd5p5D3M2mGwFDB+zZZrcFijecDsbjdiSfO6YAEM= tailscale.com v1.1.1-0.20220421175222-695f8a1d7e4d h1:lZxEfYG/4sLnGfbpizrD1fD4K6EjXsa4Jas8g5WVq8Y= tailscale.com v1.1.1-0.20220421175222-695f8a1d7e4d/go.mod h1:jqMyhjRwWu4Wzem9iVSSIjSsTMIeFXmY/Lr8a+UccbA= tailscale.com v1.1.1-0.20220423065216-bbca2c78cbd0 h1:A/Vhsbrl3xlznK4/lRZIFfp1GQKHX7b/8mT80OEFDpw= tailscale.com v1.1.1-0.20220423065216-bbca2c78cbd0/go.mod h1:5yjfQLg8gHScx/9S/SX0sim5sUKkk14HdATuAUHxGmA= tailscale.com v1.1.1-0.20220430025040-e3619b890c20 h1:NTk7u7/6JiaJV1dcOtqwPE+zREKmFfHnFK/wAFabA3Y= tailscale.com v1.1.1-0.20220430025040-e3619b890c20/go.mod h1:SjELlTggz8Lsgebu1kjfOoG+ZpYAr6t8qaxohrIPSLk= tailscale.com v1.1.1-0.20220506211610-741ae9956e67 h1:9kjMCMfJfTKJm53Ox9/1bQoLIvtYe2grCJG2d9Q00ms= tailscale.com v1.1.1-0.20220506211610-741ae9956e67/go.mod h1:2TUB66uN02iBFIH9XH3U8huzMuiulhMkWEBLOtP/L4I= tailscale.com v1.1.1-0.20220520203011-fc5839864bfb h1:ySbGnpei9LirCkocXjhGtNZlQqY8eb00rDOaAQh7k2g= tailscale.com v1.1.1-0.20220520203011-fc5839864bfb/go.mod h1:2affa2vYcxM5WnIlYtpkZsosVMZrrkF3oE5ZRx507MA= tailscale.com v1.1.1-0.20220618023759-467eb2eca09d h1:HOgwZUuK1POz5KS9bEWcEqxYRgFLa/9YqABNHfcYjso= tailscale.com v1.1.1-0.20220618023759-467eb2eca09d/go.mod h1:eTdHYI3iFv/1+rCxh60KyY2QjVToVuvbhAR9UxKuigM= tailscale.com v1.1.1-0.20220628235937-06aa14163254 h1:hNPKuQ95T48R2Myx46MOHZL+66XgCFEpShcNSN0SgMU= tailscale.com v1.1.1-0.20220628235937-06aa14163254/go.mod h1:eTdHYI3iFv/1+rCxh60KyY2QjVToVuvbhAR9UxKuigM= tailscale.com v1.1.1-0.20220706124106-e6572a0f088a h1:V7D27nnpKz2RHeluxS853jqUWSl+/FMQeCSG39otKvc= tailscale.com v1.1.1-0.20220706124106-e6572a0f088a/go.mod h1:sw/kpgrJDgUjz+aBNVCoyJa2Eksd9BbP9xusFYOc3MQ= tailscale.com v1.1.1-0.20220712041646-755396d6fe77 h1:1YQCCgvywvVuD3A1hzc4L9bYMJeG0kfbxtzZCIttbmU= tailscale.com v1.1.1-0.20220712041646-755396d6fe77/go.mod h1:bta0vbR8Cajmq+oq3as0/iAWWnWqnYv+cas4ftX9KAw= tailscale.com v1.1.1-0.20220718172352-3c892d106c2e h1:Mq8xSdoqp/UabuPt405owY20/mt3q5z7fSruQQzC9Z8= tailscale.com v1.1.1-0.20220718172352-3c892d106c2e/go.mod h1:T9uKhlkxVPdSu1Qvp882evcS/hQ1+TAyZ7sJ/VACGRI= ================================================ FILE: jni/jni.go ================================================ // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package 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) } ================================================ FILE: metadata/en-US/full_description.txt ================================================ Tailscale is a mesh VPN alternative that makes it easy to connect your devices, wherever they are. No more fighting configuration or firewall ports. Built on WireGuard®, Tailscale enables an incremental shift to zero-trust networking by implementing “always-on” remote access. This guarantees a consistent, portable, and secure experience independent of physical location. WireGuard is a registered trademark of Jason A. Donenfeld. ================================================ FILE: metadata/en-US/short_description.txt ================================================ Mesh VPN based on WireGuard ================================================ FILE: version/tailscale-version.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. # Print the version tailscale repository corresponding # to the version listed in go.mod. set -euo pipefail go_list=$(go list -m tailscale.com) # go list outputs `tailscale.com `. Extract the version. mod_version=${go_list##* } if [ -z "$mod_version" ]; then echo "no version reported by go list -m tailscale.com: $go_list" exit 1 fi case "$mod_version" in *-*-*) # A pseudo-version such as "v1.1.1-0.20201030135043-eab6e9ea4e45" # includes the commit hash. mod_version=${mod_version##*-*-} ;; esac tailscale_clone=$(mktemp -d -t tailscale-clone-XXXXXXXXXX) git clone -q https://github.com/tailscale/tailscale.git "$tailscale_clone" cd $tailscale_clone git reset --hard -q git clean -d -x -f git fetch -q --all --tags git checkout -q "$mod_version" eval $(./build_dist.sh shellvars) git_hash=$(git rev-parse HEAD) short_hash=$(echo "$git_hash" | cut -c1-9) echo ${VERSION_SHORT}-t${short_hash} cd /tmp rm -rf "$tailscale_clone"