[
  {
    "path": ".github/workflows/main.yml",
    "content": "# This is a basic workflow to help you get started with Actions\n\nname: CI\n\n# Controls when the workflow will run\non:\n  # Triggers the workflow on push or pull request events but only for the main branch\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n  # This workflow contains a single job called \"build\"\n  build:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it\n      - uses: actions/checkout@v3\n\n      # Runs a single command using the runners shell\n      - name: Create Docker container\n        run: docker build . --file Dockerfile --tag tailscale-android\n\n      # Runs a set of commands using the runners shell\n      - name: build tailscale-fdroid.apk\n        run: docker run -v /home/runner/work/tailscale-android/tailscale-android:/build/tailscale-android tailscale-android make tailscale-fdroid.apk \n        \n      - name: Upload a Build Artifact\n        uses: actions/upload-artifact@v3.0.0\n        with:\n          # Artifact name\n          name: tailscale-fdroid.apk # optional, default is artifact\n          # A file, directory or wildcard pattern that describes what to upload\n          path: /home/runner/work/tailscale-android/tailscale-android/tailscale-fdroid.apk\n          # The desired behavior if no files are found using the provided path.\n          if-no-files-found: error # optional, default is warn\n\n\n          retention-days: 30 # optional\n        \n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore Gradle project-specific cache directory\n.gradle\n\n# Ignore Gradle build output directory\nbuild\n\n# The destination for the Go Android archive.\nandroid/libs\n\n# Output files from the Makefile:\ntailscale-debug.apk\n"
  },
  {
    "path": "Dockerfile",
    "content": "# This is a Dockerfile for creating a build environment for\n# tailscale-android.\n\nFROM openjdk:8-jdk\n\n# To enable running android tools such as aapt\nRUN apt-get update && apt-get -y upgrade\nRUN apt-get install -y lib32z1 lib32stdc++6\n# For Go:\nRUN apt-get -y --no-install-recommends install curl gcc\nRUN apt-get -y --no-install-recommends install ca-certificates libc6-dev git\n\nRUN apt-get -y install make\n\nRUN mkdir -p BUILD\nENV HOME /build\n\n# Get android sdk, ndk, and rest of the stuff needed to build the android app.\nWORKDIR $HOME\nRUN mkdir android-sdk\nENV ANDROID_HOME $HOME/android-sdk\nWORKDIR $ANDROID_HOME\nRUN curl -O https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip\nRUN echo '444e22ce8ca0f67353bda4b85175ed3731cae3ffa695ca18119cbacef1c1bea0  sdk-tools-linux-3859397.zip' | sha256sum -c\nRUN unzip sdk-tools-linux-3859397.zip\nRUN echo y | $ANDROID_HOME/tools/bin/sdkmanager --update\nRUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'platforms;android-31'\nRUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'extras;android;m2repository'\nRUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk;23.1.7779620'\nRUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'platform-tools'\nRUN echo y | $ANDROID_HOME/tools/bin/sdkmanager 'build-tools;28.0.3'\n\nENV PATH $PATH:$HOME/bin:$ANDROID_HOME/platform-tools\nENV ANDROID_SDK_ROOT /build/android-sdk\n\n# We need some version of Go new enough to support the \"embed\" package\n# to run \"go run tailscale.com/cmd/printdep\" to figure out which Tailscale Go\n# version we need later, but otherwise this toolchain isn't used:\nRUN curl -L https://go.dev/dl/go1.17.5.linux-amd64.tar.gz | tar -C /usr/local -zxv\nRUN ln -s /usr/local/go/bin/go /usr/bin\n\nRUN mkdir -p $HOME/tailscale-android\nWORKDIR $HOME/tailscale-android\n\n# Preload Gradle\nCOPY android/gradlew android/gradlew\nCOPY android/gradle android/gradle\nRUN ./android/gradlew\n\nCMD /bin/bash\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Tailscale Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "Makefile",
    "content": "# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n# Use of this source code is governed by a BSD-style\n# license that can be found in the LICENSE file.\n\nDEBUG_APK=tailscale-debug.apk\nRELEASE_AAB=tailscale-release.aab\nAPPID=com.tailscale.ipn\nAAR=android/libs/ipn.aar\nKEYSTORE=tailscale.jks\nKEYSTORE_ALIAS=tailscale\nTAILSCALE_VERSION=$(shell ./version/tailscale-version.sh 200)\nOUR_VERSION=$(shell git describe --dirty --exclude \"*\" --always --abbrev=200)\nTAILSCALE_VERSION_ABBREV=$(shell ./version/tailscale-version.sh 11)\nOUR_VERSION_ABBREV=$(shell git describe --dirty --exclude \"*\" --always --abbrev=11)\nVERSION_LONG=$(TAILSCALE_VERSION_ABBREV)-g$(OUR_VERSION_ABBREV)\n# Extract the long version build.gradle's versionName and strip quotes.\nVERSIONNAME=$(patsubst \"%\",%,$(lastword $(shell grep versionName android/build.gradle)))\n# Extract the x.y.z part for the short version.\nVERSIONNAME_SHORT=$(shell echo $(VERSIONNAME) | cut -d - -f 1)\nTAILSCALE_COMMIT=$(shell echo $(TAILSCALE_VERSION) | cut -d - -f 2 | cut -d t -f 2)\n# Extract the version code from build.gradle.\nVERSIONCODE=$(lastword $(shell grep versionCode android/build.gradle))\nVERSIONCODE_PLUSONE=$(shell expr $(VERSIONCODE) + 1)\n\nTOOLCHAINREV=$(shell go run tailscale.com/cmd/printdep --go)\nTOOLCHAINDIR=${HOME}/.cache/tailscale-android-go-$(TOOLCHAINREV)\nTOOLCHAINSUM=$(shell $(TOOLCHAINDIR)/go/bin/go version >/dev/null && echo \"okay\" || echo \"bad\")\nTOOLCHAINWANT=okay\nexport PATH := $(TOOLCHAINDIR)/go/bin:$(PATH)\nexport GOROOT := # Unset\n\nall: $(APK)\n\ntag_release:\n\tsed -i'.bak' 's/versionCode [[:digit:]]\\+/versionCode $(VERSIONCODE_PLUSONE)/' android/build.gradle\n\tsed -i'.bak' 's/versionName .*/versionName \"$(VERSION_LONG)\"/' android/build.gradle\n\tgit commit -sm \"android: bump version code\" android/build.gradle\n\tgit tag -a \"$(VERSION_LONG)\"\n\nbumposs: toolchain\n\tGOPROXY=direct go get tailscale.com@main\n\tgo mod tidy -compat=1.17\n\ntoolchain:\nifneq ($(TOOLCHAINWANT),$(TOOLCHAINSUM))\n\t@echo want: $(TOOLCHAINWANT)\n\t@echo got: $(TOOLCHAINSUM)\n\trm -rf ${HOME}/.cache/tailscale-android-go-*\n\tmkdir -p $(TOOLCHAINDIR)\n\tcurl --silent -L $(shell go run tailscale.com/cmd/printdep --go-url) | tar -C $(TOOLCHAINDIR) -zx\nendif\n\n$(DEBUG_APK): toolchain\n\tmkdir -p android/libs\n\tgo run gioui.org/cmd/gogio -buildmode archive -target android -appid $(APPID) -tags novulkan -o $(AAR) github.com/tailscale/tailscale-android/cmd/tailscale\n\t(cd android && ./gradlew test assemblePlayDebug)\n\tmv android/build/outputs/apk/play/debug/android-play-debug.apk $@\n\nrundebug: $(DEBUG_APK)\n\tadb install -r $(DEBUG_APK)\n\tadb shell am start -n com.tailscale.ipn/com.tailscale.ipn.IPNActivity\n\n# tailscale-fdroid.apk builds a non-Google Play SDK, without the Google bits.\n# This is effectively what the F-Droid build definition produces.\n# This is useful for testing on e.g. Amazon Fire Stick devices.\ntailscale-fdroid.apk: toolchain\n\tmkdir -p android/libs\n\tgo run gioui.org/cmd/gogio -buildmode archive -target android -appid $(APPID) -tags novulkan -o $(AAR) github.com/tailscale/tailscale-android/cmd/tailscale\n\t(cd android && ./gradlew test assembleFdroidDebug)\n\tmv android/build/outputs/apk/fdroid/debug/android-fdroid-debug.apk $@\n\n# This target is also used by the F-Droid builder.\nrelease_aar: toolchain\nrelease_aar:\n\tmkdir -p android/libs\n\tgo 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\n\n$(RELEASE_AAB): release_aar\n\t(cd android && ./gradlew test bundlePlayRelease)\n\tmv ./android/build/outputs/bundle/playRelease/android-play-release.aab $@\n\nrelease: $(RELEASE_AAB)\n\tjarsigner -sigalg SHA256withRSA -digestalg SHA-256 -keystore $(KEYSTORE) $(RELEASE_AAB) $(KEYSTORE_ALIAS)\n\ninstall: $(DEBUG_APK)\n\tadb install -r $(DEBUG_APK)\n\ndockershell:\n\tdocker build -t tailscale-android .\n\tdocker run -v $(CURDIR):/build/tailscale-android -it --rm tailscale-android\n\nclean:\n\trm -rf android/build $(RELEASE_AAB) $(DEBUG_APK) $(AAR)\n\n.PHONY: all clean install $(DEBUG_APK) $(RELEASE_AAB) release_aar release bump_version dockershell\n"
  },
  {
    "path": "PATENTS",
    "content": "Additional IP Rights Grant (Patents)\n\n\"This implementation\" means the copyrightable works distributed by\nTailscale Inc. as part of the Tailscale project.\n\nTailscale Inc. hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable (except as stated\nin this section) patent license to make, have made, use, offer to\nsell, sell, import, transfer and otherwise run, modify and propagate\nthe contents of this implementation of Tailscale, where such license\napplies only to those patent claims, both currently owned or\ncontrolled by Tailscale Inc. and acquired in the future, licensable\nby Tailscale Inc. that are necessarily infringed by this\nimplementation of Tailscale.  This grant does not include claims that\nwould be infringed only as a consequence of further modification of\nthis implementation.  If you or your agent or exclusive licensee\ninstitute or order or agree to the institution of patent litigation\nagainst any entity (including a cross-claim or counterclaim in a\nlawsuit) alleging that this implementation of Tailscale or any code\nincorporated within this implementation of Tailscale constitutes\ndirect or contributory patent infringement, or inducement of patent\ninfringement, then any patent rights granted to you under this License\nfor this implementation of Tailscale shall terminate as of the date\nsuch litigation is filed.\n"
  },
  {
    "path": "README.md",
    "content": "# Tailscale Android Client\n\nhttps://tailscale.com\n\nPrivate WireGuard® networks made easy\n\n## Overview\n\nThis repository contains the open source Tailscale Android client.\n\n## Using\n\n[<img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on.png\"\n     alt=\"Get it on F-Droid\"\n     height=\"80\">](https://f-droid.org/packages/com.tailscale.ipn/)\n[<img src=\"https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png\"\n     alt=\"Get it on Google Play\"\n     height=\"80\">](https://play.google.com/store/apps/details?id=com.tailscale.ipn)\n\n## Building\n\n[Go](https://golang.org), the [Android\nSDK](https://developer.android.com/studio/releases/platform-tools), \nthe [Android NDK](https://developer.android.com/ndk) are required.\n\n```sh\n$ make tailscale-debug.apk\n$ adb install -r tailscale-debug.apk\n```\n\nThe `dockershell` target builds a container with the necessary\ndependencies and runs a shell inside it.\n\n```sh\n$ make dockershell\n# make tailscale-debug.apk\n```\n\nIf you have Nix 2.4 or later installed, a Nix development environment can\nbe set up with\n\n```sh\n$ alias nix='nix --extra-experimental-features \"nix-command flakes\"'\n$ nix develop\n```\n\nUse `make tag_release` to bump the Android version code, update the version\nname, and tag the current commit.\n\nWe only guarantee to support the latest Go release and any Go beta or\nrelease candidate builds (currently Go 1.14) in module mode. It might\nwork in earlier Go versions or in GOPATH mode, but we're making no\neffort to keep those working.\n\n## Google Sign-In\n\nGoogle Sign-In support relies on configuring a [Google API Console\nproject](https://developers.google.com/identity/sign-in/android/start-integrating)\nwith the app identifier and [signing key\nhashes](https://developers.google.com/android/guides/client-auth).\nThe official release uses the app identifier `com.tailscale.ipn`;\ncustom builds should use a different identifier.\n\n## Running in the Android emulator\n\nBy default, the android emulator uses an older version of OpenGL ES,\nwhich results in a black screen when opening the Tailscale app. To fix\nthis, with the emulator running:\n\n - Open the three-dots menu to access emulator settings\n - To to `Settings > Advanced`\n - Set \"OpenGL ES API level\" to \"Renderer maximum (up to OpenGL ES 3.1)\"\n - Close the emulator.\n - In Android Studio's emulator view (that lists all your emulated\n   devices), hit the down arrow by the virtual device and select \"Cold\n   boot now\" to restart the emulator from scratch.\n\nThe Tailscale app should now render correctly.\n\nAdditionally, there seems to be a bug that prevents using the\nsystem-level Google sign-in option (the one that pops up a\nsystem-level UI to select your Google account). You can work around\nthis by selecting \"Other\" at the sign-in screen, and then selecting\nGoogle from the next screen.\n\n## Developing on a Fire Stick TV\n\nOn the Fire Stick:\n\n* Settings > My Fire TV > Developer Options > ADB Debugging > ON\n\nThen some useful commands:\n```\nadb connect 10.2.200.213:5555\nadb install -r tailscale-fdroid.apk\nadb shell am start -n com.tailscale.ipn/com.tailscale.ipn.IPNActivity\nadb shell pm uninstall com.tailscale.ipn\n```\n\n## Building on macOS\n\nTo build from the CLI on macOS:\n\n1. Install Android Studio\n2. In Android Studio's home screen: \"More Actions\" > \"SDK Manager\", install NDK.\n3. You can now close Android Studio, unless you want it to create virtual devices\n   (\"More Actions\" > \"Virtual Device Manager\").\n4. Then, from CLI:\n5. `export JAVA_HOME='/Applications/Android Studio.app/Contents/jre/Contents/Home'`\n6. `export ANDROID_SDK_ROOT=$HOME/Library/Android/sdk`\n7. `make tailscale-fdroid.apk`, etc\n\n## Bugs\n\nPlease file any issues about this code or the hosted service on\n[the tailscale issue tracker](https://github.com/tailscale/tailscale/issues).\n\n## Contributing\n\n`under_construction.gif`\n\nPRs welcome, but we are still working out our contribution process and\ntooling.\n\nWe require [Developer Certificate of\nOrigin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)\n`Signed-off-by` lines in commits.\n\n## About Us\n\nWe are apenwarr, bradfitz, crawshaw, danderson, dfcarney,\nfrom Tailscale Inc.\nYou can learn more about us from [our website](https://tailscale.com).\n\nWireGuard is a registered trademark of Jason A. Donenfeld.\n"
  },
  {
    "path": "android/build.gradle",
    "content": "buildscript {\n\trepositories {\n\t\tgoogle()\n\t\tjcenter()\n\t}\n\tdependencies {\n\t\tclasspath 'com.android.tools.build:gradle:4.2.0'\n\t}\n}\n\nallprojects {\n\trepositories {\n\t\tgoogle()\n\t\tjcenter()\n\t\tflatDir {\n\t\t\tdirs 'libs'\n\t\t}\n\t}\n}\n\napply plugin: 'com.android.application'\n\nandroid {\n\tndkVersion \"23.1.7779620\"\n\tcompileSdkVersion 30\n\tdefaultConfig {\n\t\tminSdkVersion 22\n\t\ttargetSdkVersion 30\n\t\tversionCode 114\n\t\tversionName \"1.29.0-t3c892d106-g42f688f1292\"\n\t}\n\tcompileOptions {\n\t\tsourceCompatibility 1.8\n\t\ttargetCompatibility 1.8\n\t}\n\tflavorDimensions \"version\"\n\tproductFlavors {\n\t\tfdroid {\n\t\t\t// The fdroid flavor contains only free dependencies and is suitable\n\t\t\t// for the F-Droid app store.\n\t\t}\n\t\tplay {\n\t\t\t// The play flavor contains all features and is for the Play Store.\n\t\t}\n\t}\n}\n\ndependencies {\n\timplementation \"androidx.core:core:1.2.0\"\n\timplementation \"androidx.browser:browser:1.2.0\"\n\timplementation \"androidx.security:security-crypto:1.1.0-alpha03\"\n\timplementation ':ipn@aar'\n\ttestCompile \"junit:junit:4.12\"\n\n\t// Non-free dependencies.\n\tplayImplementation 'com.google.android.gms:play-services-auth:18.0.0'\n}\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionSha256Sum=3239b5ed86c3838a37d983ac100573f64c1f3fd8e1eb6c89fa5f9529b5ec091d\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.7.1-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "android.useAndroidX=true\n"
  },
  {
    "path": "android/gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "android/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "android/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tpackage=\"com.tailscale.ipn\">\n\t<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n\t<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n\t<uses-permission android:name=\"android.permission.INTERNET\" />\n\t<uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n\t<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" android:maxSdkVersion=\"28\"/>\n\n\t<!-- Disable input emulation on ChromeOS -->\n\t<uses-feature android:name=\"android.hardware.type.pc\" android:required=\"false\"/>\n\n\t<!-- Signal support for Android TV -->\n\t<uses-feature android:name=\"android.software.leanback\" android:required=\"false\" />\n\t<uses-feature android:name=\"android.hardware.touchscreen\" android:required=\"false\" />\n\n\t<application android:label=\"Tailscale\" android:icon=\"@mipmap/ic_launcher\" android:roundIcon=\"@mipmap/ic_launcher_round\"\n                     android:banner=\"@drawable/tv_banner\"\n                     android:name=\".App\" android:allowBackup=\"false\">\n\t\t<activity android:name=\"IPNActivity\"\n\t\t\tandroid:label=\"@string/app_name\"\n\t\t\tandroid:theme=\"@style/Theme.GioApp\"\n\t\t\tandroid:configChanges=\"orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden\"\n\t\t\tandroid:windowSoftInputMode=\"adjustResize\"\n\t\t\tandroid:launchMode=\"singleTask\">\n\t\t\t<intent-filter>\n\t\t\t\t<action android:name=\"android.intent.action.MAIN\" />\n\t\t\t\t<category android:name=\"android.intent.category.LAUNCHER\" />\n\t\t\t\t<category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n\t\t\t</intent-filter>\n\t\t\t<intent-filter>\n\t\t\t\t<action android:name=\"android.service.quicksettings.action.QS_TILE_PREFERENCES\" />\n\t\t\t</intent-filter>\n\t\t\t<intent-filter>\n\t\t\t\t<action android:name=\"android.intent.action.SEND\" />\n\t\t\t\t<category android:name=\"android.intent.category.DEFAULT\"/>\n\t\t\t\t<data android:mimeType=\"application/*\" />\n\t\t\t\t<data android:mimeType=\"audio/*\" />\n\t\t\t\t<data android:mimeType=\"image/*\" />\n\t\t\t\t<data android:mimeType=\"message/*\" />\n\t\t\t\t<data android:mimeType=\"multipart/*\" />\n\t\t\t\t<data android:mimeType=\"text/*\" />\n\t\t\t\t<data android:mimeType=\"video/*\" />\n\t\t\t</intent-filter>\n\t\t\t<intent-filter>\n\t\t\t\t<action android:name=\"android.intent.action.SEND_MULTIPLE\" />\n\t\t\t\t<category android:name=\"android.intent.category.DEFAULT\" />\n\t\t\t\t<data android:mimeType=\"application/*\" />\n\t\t\t\t<data android:mimeType=\"audio/*\" />\n\t\t\t\t<data android:mimeType=\"image/*\" />\n\t\t\t\t<data android:mimeType=\"message/*\" />\n\t\t\t\t<data android:mimeType=\"multipart/*\" />\n\t\t\t\t<data android:mimeType=\"text/*\" />\n\t\t\t\t<data android:mimeType=\"video/*\" />\n\t\t\t</intent-filter>\n\t\t</activity>\n\t\t<service android:name=\".IPNService\"\n\t\t\tandroid:permission=\"android.permission.BIND_VPN_SERVICE\">\n\t\t\t<intent-filter>\n\t\t\t\t<action android:name=\"android.net.VpnService\"/>\n\t\t\t</intent-filter>\n\t\t</service>\n\t\t<service\n\t\t\tandroid:name=\".QuickToggleService\"\n\t\t\tandroid:icon=\"@drawable/ic_tile\"\n\t\t\tandroid:label=\"@string/tile_name\"\n\t\t\tandroid:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n\t\t<intent-filter>\n\t\t\t<action android:name=\"android.service.quicksettings.action.QS_TILE\"/>\n\t\t</intent-filter>\n\t</service>\n\t</application>\n</manifest>\n\n"
  },
  {
    "path": "android/src/main/java/com/tailscale/ipn/App.java",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage com.tailscale.ipn;\n\nimport android.app.Application;\nimport android.app.Activity;\nimport android.app.DownloadManager;\nimport android.app.Fragment;\nimport android.app.FragmentTransaction;\nimport android.app.NotificationChannel;\nimport android.app.PendingIntent;\nimport android.app.UiModeManager;\nimport android.content.BroadcastReceiver;\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.content.SharedPreferences;\nimport android.content.pm.PackageManager;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.Signature;\nimport android.content.res.Configuration;\nimport android.provider.MediaStore;\nimport android.provider.Settings;\nimport android.net.ConnectivityManager;\nimport android.net.LinkProperties;\nimport android.net.Network;\nimport android.net.NetworkInfo;\nimport android.net.NetworkRequest;\nimport android.net.Uri;\nimport android.net.VpnService;\nimport android.view.View;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport android.Manifest;\nimport android.webkit.MimeTypeMap;\n\nimport java.io.IOException;\nimport java.io.File;\nimport java.io.FileOutputStream;\n\nimport java.lang.StringBuilder;\n\nimport java.net.InetAddress;\nimport java.net.InterfaceAddress;\nimport java.net.NetworkInterface;\n\nimport java.security.GeneralSecurityException;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\n\nimport androidx.core.app.NotificationCompat;\nimport androidx.core.app.NotificationManagerCompat;\n\nimport androidx.core.content.ContextCompat;\n\nimport androidx.security.crypto.EncryptedSharedPreferences;\nimport androidx.security.crypto.MasterKey;\n\nimport androidx.browser.customtabs.CustomTabsIntent;\n\nimport org.gioui.Gio;\n\npublic class App extends Application {\n\tprivate final static String PEER_TAG = \"peer\";\n\n\tstatic final String STATUS_CHANNEL_ID = \"tailscale-status\";\n\tstatic final int STATUS_NOTIFICATION_ID = 1;\n\n\tstatic final String NOTIFY_CHANNEL_ID = \"tailscale-notify\";\n\tstatic final int NOTIFY_NOTIFICATION_ID = 2;\n\n\tprivate static final String FILE_CHANNEL_ID = \"tailscale-files\";\n\tprivate static final int FILE_NOTIFICATION_ID = 3;\n\n\tprivate final static Handler mainHandler = new Handler(Looper.getMainLooper());\n\n\tpublic DnsConfig dns = new DnsConfig(this);\n\tpublic DnsConfig getDnsConfigObj() { return this.dns; }\n\n\t@Override public void onCreate() {\n\t\tsuper.onCreate();\n\t\t// Load and initialize the Go library.\n\t\tGio.init(this);\n\t\tregisterNetworkCallback();\n\n\t\tcreateNotificationChannel(NOTIFY_CHANNEL_ID, \"Notifications\", NotificationManagerCompat.IMPORTANCE_DEFAULT);\n\t\tcreateNotificationChannel(STATUS_CHANNEL_ID, \"VPN Status\", NotificationManagerCompat.IMPORTANCE_LOW);\n\t\tcreateNotificationChannel(FILE_CHANNEL_ID, \"File transfers\", NotificationManagerCompat.IMPORTANCE_DEFAULT);\n\n\t}\n\n\tprivate void registerNetworkCallback() {\n\t\tConnectivityManager cMgr = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);\n\t\tcMgr.registerNetworkCallback(new NetworkRequest.Builder().build(), new ConnectivityManager.NetworkCallback() {\n\t\t\tprivate void reportConnectivityChange() {\n\t\t\t\tNetworkInfo active = cMgr.getActiveNetworkInfo();\n\t\t\t\t// https://developer.android.com/training/monitoring-device-state/connectivity-status-type\n\t\t\t\tboolean isConnected = active != null && active.isConnectedOrConnecting();\n\t\t\t\tonConnectivityChanged(isConnected);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void onLost(Network network) {\n\t\t\t\tsuper.onLost(network);\n\t\t\t\tthis.reportConnectivityChange();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {\n\t\t\t\tsuper.onLinkPropertiesChanged(network, linkProperties);\n\t\t\t\tthis.reportConnectivityChange();\n\t\t\t}\n\t\t});\n\t}\n\n\tpublic void startVPN() {\n\t\tIntent intent = new Intent(this, IPNService.class);\n\t\tintent.setAction(IPNService.ACTION_CONNECT);\n\t\tstartService(intent);\n\t}\n\n\tpublic void stopVPN() {\n\t\tIntent intent = new Intent(this, IPNService.class);\n\t\tintent.setAction(IPNService.ACTION_DISCONNECT);\n\t\tstartService(intent);\n\t}\n\n\t// encryptToPref a byte array of data using the Jetpack Security\n\t// library and writes it to a global encrypted preference store.\n\tpublic void encryptToPref(String prefKey, String plaintext) throws IOException, GeneralSecurityException {\n\t\tgetEncryptedPrefs().edit().putString(prefKey, plaintext).commit();\n\t}\n\n\t// decryptFromPref decrypts a encrypted preference using the Jetpack Security\n\t// library and returns the plaintext.\n\tpublic String decryptFromPref(String prefKey) throws IOException, GeneralSecurityException {\n\t\treturn getEncryptedPrefs().getString(prefKey, null);\n\t}\n\n\tprivate SharedPreferences getEncryptedPrefs() throws IOException, GeneralSecurityException {\n\t\tMasterKey key = new MasterKey.Builder(this)\n\t\t\t.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n\t\t\t.build();\n\n\t\treturn EncryptedSharedPreferences.create(\n\t\t\tthis,\n\t\t\t\"secret_shared_prefs\",\n\t\t\tkey,\n\t\t\tEncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n\t\t\tEncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n\t\t);\n\t}\n\n\tvoid setTileReady(boolean ready) {\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {\n\t\t\treturn;\n\t\t}\n\t\tQuickToggleService.setReady(this, ready);\n\t}\n\n\tvoid setTileStatus(boolean status) {\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {\n\t\t\treturn;\n\t\t}\n\t\tQuickToggleService.setStatus(this, status);\n\t}\n\n\tString getHostname() {\n\t\tString userConfiguredDeviceName = getUserConfiguredDeviceName();\n\t\tif (!isEmpty(userConfiguredDeviceName)) return userConfiguredDeviceName;\n\n\t\treturn getModelName();\n\t}\n\n\tString getModelName() {\n\t\tString manu = Build.MANUFACTURER;\n\t\tString model = Build.MODEL;\n\t\t// Strip manufacturer from model.\n\t\tint idx = model.toLowerCase().indexOf(manu.toLowerCase());\n\t\tif (idx != -1) {\n\t\t\tmodel = model.substring(idx + manu.length());\n\t\t\tmodel = model.trim();\n\t\t}\n\t\treturn manu + \" \" + model;\n\t}\n\n\tString getOSVersion() {\n\t\treturn Build.VERSION.RELEASE;\n\t}\n\n\t// get user defined nickname from Settings\n\t// returns null if not available\n\tprivate String getUserConfiguredDeviceName() {\n\t\tString nameFromSystemBluetooth = Settings.System.getString(getContentResolver(), \"bluetooth_name\");\n\t\tString nameFromSecureBluetooth = Settings.Secure.getString(getContentResolver(), \"bluetooth_name\");\n\t\tString nameFromSystemDevice = Settings.Secure.getString(getContentResolver(), \"device_name\");\n\n\t\tif (!isEmpty(nameFromSystemBluetooth)) return nameFromSystemBluetooth;\n\t\tif (!isEmpty(nameFromSecureBluetooth)) return nameFromSecureBluetooth;\n\t\tif (!isEmpty(nameFromSystemDevice)) return nameFromSystemDevice;\n\t\treturn null;\n\t}\n\n\tprivate static boolean isEmpty(String str) {\n\t\treturn str == null || str.length() == 0;\n\t}\n\n\t// attachPeer adds a Peer fragment for tracking the Activity\n\t// lifecycle.\n\tvoid attachPeer(Activity act) {\n\t\tact.runOnUiThread(new Runnable() {\n\t\t\t@Override public void run() {\n\t\t\t\tFragmentTransaction ft = act.getFragmentManager().beginTransaction();\n\t\t\t\tft.add(new Peer(), PEER_TAG);\n\t\t\t\tft.commit();\n\t\t\t\tact.getFragmentManager().executePendingTransactions();\n\t\t\t}\n\t\t});\n\t}\n\n\tboolean isChromeOS() {\n\t\treturn getPackageManager().hasSystemFeature(\"android.hardware.type.pc\");\n\t}\n\n\tvoid prepareVPN(Activity act, int reqCode) {\n\t\tact.runOnUiThread(new Runnable() {\n\t\t\t@Override public void run() {\n\t\t\t\tIntent intent = VpnService.prepare(act);\n\t\t\t\tif (intent == null) {\n\t\t\t\t\tonVPNPrepared();\n\t\t\t\t} else {\n\t\t\t\t\tstartActivityForResult(act, intent, reqCode);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tstatic void startActivityForResult(Activity act, Intent intent, int request) {\n\t\tFragment f = act.getFragmentManager().findFragmentByTag(PEER_TAG);\n\t\tf.startActivityForResult(intent, request);\n\t}\n\n\tvoid showURL(Activity act, String url) {\n\t\tact.runOnUiThread(new Runnable() {\n\t\t\t@Override public void run() {\n\t\t\t\tCustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();\n\t\t\t\tint headerColor = 0xff496495;\n\t\t\t\tbuilder.setToolbarColor(headerColor);\n\t\t\t\tCustomTabsIntent intent = builder.build();\n\t\t\t\tintent.launchUrl(act, Uri.parse(url));\n\t\t\t}\n\t\t});\n\t}\n\n\t// getPackageSignatureFingerprint returns the first package signing certificate, if any.\n\tbyte[] getPackageCertificate() throws Exception {\n\t\tPackageInfo info;\n\t\tinfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);\n\t\tfor (Signature signature : info.signatures) {\n\t\t\treturn signature.toByteArray();\n\t\t}\n\t\treturn null;\n\t}\n\n\tvoid requestWriteStoragePermission(Activity act) {\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n\t\t\t// We can write files without permission.\n\t\t\treturn;\n\t\t}\n\t\tif (ContextCompat.checkSelfPermission(act, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {\n\t\t\treturn;\n\t\t}\n\t\tact.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, IPNActivity.WRITE_STORAGE_RESULT);\n\t}\n\n\tString insertMedia(String name, String mimeType) throws IOException {\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n\t\t\tContentResolver resolver = getContentResolver();\n\t\t\tContentValues contentValues = new ContentValues();\n\t\t\tcontentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);\n\t\t\tif (!\"\".equals(mimeType)) {\n\t\t\t\tcontentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);\n\t\t\t}\n\t\t\tUri root = MediaStore.Files.getContentUri(\"external\");\n\t\t\treturn resolver.insert(root, contentValues).toString();\n\t\t} else {\n\t\t\tFile dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);\n\t\t\tdir.mkdirs();\n\t\t\tFile f = new File(dir, name);\n\t\t\treturn Uri.fromFile(f).toString();\n\t\t}\n\t}\n\n\tint openUri(String uri, String mode) throws IOException {\n\t\tContentResolver resolver = getContentResolver();\n\t\treturn resolver.openFileDescriptor(Uri.parse(uri), mode).detachFd();\n\t}\n\n\tvoid deleteUri(String uri) {\n\t\tContentResolver resolver = getContentResolver();\n\t\tresolver.delete(Uri.parse(uri), null, null);\n\t}\n\n\tpublic void notifyFile(String uri, String msg) {\n\t\tIntent viewIntent;\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n\t\t\tviewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));\n\t\t} else {\n\t\t\t// uri is a file:// which is not allowed to be shared outside the app.\n\t\t\tviewIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);\n\t\t}\n\t\tPendingIntent pending = PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT);\n\t\tNotificationCompat.Builder builder = new NotificationCompat.Builder(this, FILE_CHANNEL_ID)\n\t\t\t.setSmallIcon(R.drawable.ic_notification)\n\t\t\t.setContentTitle(\"File received\")\n\t\t\t.setContentText(msg)\n\t\t\t.setContentIntent(pending)\n\t\t\t.setAutoCancel(true)\n\t\t\t.setOnlyAlertOnce(true)\n\t\t\t.setPriority(NotificationCompat.PRIORITY_DEFAULT);\n\n\t\tNotificationManagerCompat nm = NotificationManagerCompat.from(this);\n\t\tnm.notify(FILE_NOTIFICATION_ID, builder.build());\n\t}\n\n\tprivate void createNotificationChannel(String id, String name, int importance) {\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {\n\t\t\treturn;\n\t\t}\n\t\tNotificationChannel channel = new NotificationChannel(id, name, importance);\n\t\tNotificationManagerCompat nm = NotificationManagerCompat.from(this);\n\t\tnm.createNotificationChannel(channel);\n\t}\n\n\tstatic native void onVPNPrepared();\n\tprivate static native void onConnectivityChanged(boolean connected);\n\tstatic native void onShareIntent(int nfiles, int[] types, String[] mimes, String[] items, String[] names, long[] sizes);\n\tstatic native void onWriteStorageGranted();\n\n        // Returns details of the interfaces in the system, encoded as a single string for ease\n        // of JNI transfer over to the Go environment.\n        //\n        // Example:\n        // rmnet_data0 10 2000 true false false false false | fe80::4059:dc16:7ed3:9c6e%rmnet_data0/64\n        // dummy0 3 1500 true false false false false | fe80::1450:5cff:fe13:f891%dummy0/64\n        // wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24\n        // r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64\n        // rmnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64\n        // r_rmnet_data1 22 1500 true false false false false | fe80::b6cd:5cb0:8ae6:fe92%r_rmnet_data1/64\n        // rmnet_data1 11 1500 true false false false false | fe80::51f2:ee00:edce:d68b%rmnet_data1/64\n        // lo 1 65536 true false true false false | ::1/128 127.0.0.1/8\n        // v4-rmnet_data2 68 1472 true true false true true | 192.0.0.4/32\n        //\n        // Where the fields are:\n        // name ifindex mtu isUp hasBroadcast isLoopback isPointToPoint hasMulticast | ip1/N ip2/N ip3/N;\n\tString getInterfacesAsString() {\n            List<NetworkInterface> interfaces;\n            try {\n                interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());\n            } catch (Exception e) {\n                return \"\";\n            }\n\n            StringBuilder sb = new StringBuilder(\"\");\n            for (NetworkInterface nif : interfaces) {\n                try {\n                    // Android doesn't have a supportsBroadcast() but the Go net.Interface wants\n                    // one, so we say the interface has broadcast if it has multicast.\n                    sb.append(String.format(java.util.Locale.ROOT, \"%s %d %d %b %b %b %b %b |\", nif.getName(),\n                                   nif.getIndex(), nif.getMTU(), nif.isUp(), nif.supportsMulticast(),\n                                   nif.isLoopback(), nif.isPointToPoint(), nif.supportsMulticast()));\n\n                    for (InterfaceAddress ia : nif.getInterfaceAddresses()) {\n                        // InterfaceAddress == hostname + \"/\" + IP\n                        String[] parts = ia.toString().split(\"/\", 0);\n                        if (parts.length > 1) {\n                            sb.append(String.format(java.util.Locale.ROOT, \"%s/%d \", parts[1], ia.getNetworkPrefixLength()));\n                        }\n                    }\n                } catch (Exception e) {\n                    // TODO(dgentry) should log the exception not silently suppress it.\n                    continue;\n                }\n                sb.append(\"\\n\");\n            }\n\n            return sb.toString();\n        }\n\n\tboolean isTV() {\n\t\tUiModeManager mm = (UiModeManager)getSystemService(UI_MODE_SERVICE);\n\t\treturn mm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;\n\t}\n}\n"
  },
  {
    "path": "android/src/main/java/com/tailscale/ipn/DnsConfig.java",
    "content": "// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage com.tailscale.ipn;\n\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.DhcpInfo;\nimport android.net.LinkProperties;\nimport android.net.Network;\nimport android.net.NetworkCapabilities;\nimport android.net.NetworkInfo;\nimport android.net.wifi.WifiManager;\n\nimport java.lang.reflect.Method;\n\nimport java.net.InetAddress;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\n// Tailscale DNS Config retrieval\n//\n// Tailscale's DNS support can either override the local DNS servers with a set of servers\n// configured in the admin panel, or supplement the local DNS servers with additional\n// servers for specific domains like example.com.beta.tailscale.net. In the non-override mode,\n// we need to retrieve the current set of DNS servers from the platform. These will typically\n// be the DNS servers received from DHCP.\n//\n// Importantly, after the Tailscale VPN comes up it will set a DNS server of 100.100.100.100\n// but we still want to retrieve the underlying DNS servers received from DHCP. If we roam\n// from Wi-Fi to LTE, we want the DNS servers received from LTE.\n//\n// --------------------- Android 7 and later -----------------------------------------\n//\n// ## getDnsConfigFromLinkProperties\n// Android provides a getAllNetworks interface in the ConnectivityManager. We walk through\n// each interface to pick the most appropriate one.\n// - If there is an Ethernet interface active we use that.\n// - If Wi-Fi is active we use that.\n// - If LTE is active we use that.\n// - We never use a VPN's DNS servers. That VPN is likely us. Even if not us, Android\n// only allows one VPN at a time so a different VPN's DNS servers won't be available\n// once Tailscale comes up.\n//\n// getAllNetworks() is used as the sole mechanism for retrieving the DNS config with\n// Android 7 and later.\n//\n// --------------------- Releases older than Android 7 -------------------------------\n//\n// We support Tailscale back to Android 5. Android versions 5 and 6 supply a getAllNetworks()\n// implementation but it always returns an empty list.\n//\n// ## getDnsConfigFromLinkProperties with getActiveNetwork\n// ConnectivityManager also supports a getActiveNetwork() routine, which Android 5 and 6 do\n// return a value for. If Tailscale isn't up yet and we can get the Wi-Fi/LTE/etc DNS\n// config using getActiveNetwork(), we use that.\n//\n// Once Tailscale is up, getActiveNetwork() returns tailscale0 with DNS server 100.100.100.100\n// and that isn't useful. So we try two other mechanisms:\n//\n// ## getDnsServersFromSystemProperties\n// Android versions prior to 8 let us retrieve the actual system DNS servers from properties.\n// Later Android versions removed the properties and only return an empty string.\n//\n// We check the net.dns1 - net.dns4 DNS servers. If Tailscale is up the DNS server will be\n// 100.100.100.100, which isn't useful, but if we get something different we'll use that.\n//\n// getDnsServersFromSystemProperties can only retrieve the IPv4 or IPv6 addresses of the\n// configured DNS servers. We also want to know the DNS Search Domains configured, but\n// we have no way to retrieve this using these interfaces. We return an empty list of\n// search domains. Sorry.\n//\n// ## getDnsServersFromNetworkInfo\n// ConnectivityManager supports an older API called getActiveNetworkInfo to return the\n// active network interface. It doesn't handle VPNs, so the interface will always be Wi-Fi\n// or Cellular even if Tailscale is up.\n//\n// For Wi-Fi interfaces we retrieve the DHCP response from the WifiManager. For Cellular\n// interfaces we check for properties populated by most of the radio drivers.\n//\n// getDnsServersFromNetworkInfo does not have a way to retrieve the DNS Search Domains,\n// so we return an empty list. Additionally, these interfaces are so old that they only\n// support IPv4. We can't retrieve IPv6 DNS server addresses this way.\n\npublic class DnsConfig {\n\tprivate Context ctx;\n\n\tpublic DnsConfig(Context ctx) {\n\t\tthis.ctx = ctx;\n\t}\n\n\t// getDnsConfigAsString returns the current DNS configuration as a multiline string:\n\t// line[0] DNS server addresses separated by spaces\n\t// line[1] search domains separated by spaces\n\t//\n\t// For example:\n\t// 8.8.8.8 8.8.4.4\n\t// example.com\n\t//\n\t// an empty string means the current DNS configuration could not be retrieved.\n\tString getDnsConfigAsString() {\n\t\tString s = getDnsConfigFromLinkProperties();\n\t\tif (!s.trim().isEmpty()) {\n\t\t\treturn s;\n\t\t}\n\t\tif (android.os.Build.VERSION.SDK_INT >= 23) {\n\t\t\t// If ConnectivityManager.getAllNetworks() works, it is the\n\t\t\t// authoritative mechanism and we rely on it. The other methods\n\t\t\t// which follow involve more compromises.\n\t\t\treturn \"\";\n\t\t}\n\n\t\ts = getDnsServersFromSystemProperties();\n\t\tif (!s.trim().isEmpty()) {\n\t\t\treturn s;\n\t\t}\n\t\treturn getDnsServersFromNetworkInfo();\n\t}\n\n\t// getDnsConfigFromLinkProperties finds the DNS servers for each Network interface\n\t// returned by ConnectivityManager getAllNetworks().LinkProperties, and return the\n\t// one that (heuristically) would be the primary DNS servers.\n\t//\n\t// on a Nexus 4  with Android  5.1 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1\n\t// on a Nexus 7  with Android  6.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1\n\t// on a Pixel 3a with Android 12.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1\\nlocaldomain\n\t// on a Pixel 3a with Android 12.0 on LTE:  fd00:976a::9 fd00:976a::10\n\t//\n\t// One odd behavior noted on Pixel3a with Android 12:\n\t// With Wi-Fi already connected, starting Tailscale returned DNS servers 2602:248:7b4a:ff60::1 10.1.10.1\n\t// Turning off Wi-Fi and connecting LTE returned DNS servers fd00:976a::9 fd00:976a::10.\n\t// Turning Wi-Fi back on return DNS servers: 10.1.10.1. The IPv6 DNS server is gone.\n\t// This appears to be the ConnectivityManager behavior, not something we are doing.\n\t//\n\t// This implementation can work through Android 12 (SDK 30). In SDK 31 the\n\t// getAllNetworks() method is deprecated and we'll need to implement a\n\t// android.net.ConnectivityManager.NetworkCallback instead to monitor\n\t// link changes and track which DNS server to use.\n\tString getDnsConfigFromLinkProperties() {\n\t\tConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);\n\t\tif (cMgr == null) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tNetwork[] networks = cMgr.getAllNetworks();\n\t\tif (networks == null) {\n\t\t\t// Android 6 and before often returns an empty list, but we\n\t\t\t// can try again with just the active network.\n\t\t\t//\n\t\t\t// Once Tailscale is connected, the active network will be Tailscale\n\t\t\t// which will have 100.100.100.100 for its DNS server. We reject\n\t\t\t// TYPE_VPN in getPreferabilityForNetwork, so it won't be returned.\n\t\t\tNetwork active = cMgr.getActiveNetwork();\n\t\t\tif (active == null) {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tnetworks = new Network[]{active};\n\t\t}\n\n\t\t// getPreferabilityForNetwork returns an index into dnsConfigs from 0-3.\n\t\tString[] dnsConfigs = new String[]{\"\", \"\", \"\", \"\"};\n\t\tfor (Network network : networks) {\n\t\t\tint idx = getPreferabilityForNetwork(cMgr, network);\n\t\t\tif ((idx < 0) || (idx > 3)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tLinkProperties linkProp = cMgr.getLinkProperties(network);\n\t\t\tNetworkCapabilities nc = cMgr.getNetworkCapabilities(network);\n\t\t\tList<InetAddress> dnsList = linkProp.getDnsServers();\n\t\t\tStringBuilder sb = new StringBuilder(\"\");\n\t\t\tfor (InetAddress ip : dnsList) {\n\t\t\t\tsb.append(ip.getHostAddress() + \" \");\n\t\t\t}\n\n\t\t\tString d = linkProp.getDomains();\n\t\t\tif (d != null) {\n\t\t\t\tsb.append(\"\\n\");\n\t\t\t\tsb.append(d);\n\t\t\t}\n\n\t\t\tdnsConfigs[idx] = sb.toString();\n\t\t}\n\n\t\t// return the lowest index DNS config which exists. If an Ethernet config\n\t\t// was found, return it. Otherwise if Wi-fi was found, return it. Etc.\n\t\tfor (String s : dnsConfigs) {\n\t\t\tif (!s.trim().isEmpty()) {\n\t\t\t\treturn s;\n\t\t\t}\n\t\t}\n\n\t\treturn \"\";\n\t}\n\n\t// getDnsServersFromSystemProperties returns DNS servers found in system properties.\n\t// On Android versions prior to Android 8, we can directly query the DNS\n\t// servers the system is using. More recent Android releases return empty strings.\n\t//\n\t// Once Tailscale is connected these properties will return 100.100.100.100, which we\n\t// suppress.\n\t//\n\t// on a Nexus 4  with Android  5.1 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1\n\t// on a Nexus 7  with Android  6.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1\n\t// on a Pixel 3a with Android 12.0 on wifi:\n\t// on a Pixel 3a with Android 12.0 on  LTE:\n\t//\n\t// The list of DNS search domains does not appear to be available in system properties.\n\tString getDnsServersFromSystemProperties() {\n\t\ttry {\n\t\t\tClass SystemProperties = Class.forName(\"android.os.SystemProperties\");\n\t\t\tMethod method = SystemProperties.getMethod(\"get\", String.class);\n\t\t\tList<String> servers = new ArrayList<String>();\n\t\t\tfor (String name : new String[]{\"net.dns1\", \"net.dns2\", \"net.dns3\", \"net.dns4\"}) {\n\t\t\t\tString value = (String) method.invoke(null, name);\n\t\t\t\tif (value != null && !value.isEmpty() &&\n\t\t\t\t\t\t!value.equals(\"100.100.100.100\") &&\n\t\t\t\t\t\t!servers.contains(value)) {\n\t\t\t\t\tservers.add(value);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn String.join(\" \", servers);\n\t\t} catch (Exception e) {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\n\n\tpublic String intToInetString(int hostAddress) {\n\t\treturn String.format(java.util.Locale.ROOT, \"%d.%d.%d.%d\",\n\t\t\t(0xff & hostAddress),\n\t\t\t(0xff & (hostAddress >> 8)),\n\t\t\t(0xff & (hostAddress >> 16)),\n\t\t\t(0xff & (hostAddress >> 24)));\n\t}\n\n\t// getDnsServersFromNetworkInfo retrieves DNS servers using ConnectivityManager\n\t// getActiveNetworkInfo() plus interface-specific mechanisms to retrieve the DNS servers.\n\t// Only IPv4 DNS servers are supported by this mechanism, neither the WifiManager nor the\n\t// interface-specific dns properties appear to populate IPv6 DNS server addresses.\n\t//\n\t// on a Nexus 4  with Android 5.1  on wifi: 10.1.10.1\n\t// on a Nexus 7  with Android 6.0  on wifi: 10.1.10.1\n\t// on a Pixel-3a with Android 12.0 on wifi: 10.1.10.1\n\t// on a Pixel-3a with Android 12.0 on  LTE:\n\t//\n\t// The list of DNS search domains is not available in this way.\n\tString getDnsServersFromNetworkInfo() {\n\t\tConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);\n\t\tif (cMgr == null) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tNetworkInfo info = cMgr.getActiveNetworkInfo();\n\t\tif (info == null) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tClass SystemProperties;\n\t\tMethod method;\n\n\t\ttry {\n\t\t\tSystemProperties = Class.forName(\"android.os.SystemProperties\");\n\t\t\tmethod = SystemProperties.getMethod(\"get\", String.class);\n\t\t} catch (Exception e) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tList<String> servers = new ArrayList<String>();\n\n\t\tswitch(info.getType()) {\n\t\tcase ConnectivityManager.TYPE_WIFI:\n\t\tcase ConnectivityManager.TYPE_WIMAX:\n\t\t\tfor (String name : new String[]{\n\t\t\t\t\"net.wifi0.dns1\", \"net.wifi0.dns2\", \"net.wifi0.dns3\", \"net.wifi0.dns4\",\n\t\t\t\t\"net.wlan0.dns1\", \"net.wlan0.dns2\", \"net.wlan0.dns3\", \"net.wlan0.dns4\",\n\t\t\t\t\"net.eth0.dns1\", \"net.eth0.dns2\", \"net.eth0.dns3\", \"net.eth0.dns4\",\n\t\t\t\t\"dhcp.wlan0.dns1\", \"dhcp.wlan0.dns2\", \"dhcp.wlan0.dns3\", \"dhcp.wlan0.dns4\",\n\t\t\t\t\"dhcp.tiwlan0.dns1\", \"dhcp.tiwlan0.dns2\", \"dhcp.tiwlan0.dns3\", \"dhcp.tiwlan0.dns4\"}) {\n\t\t\t\ttry {\n\t\t\t\t\tString value = (String) method.invoke(null, name);\n\t\t\t\t\tif (value != null && !value.isEmpty() && !servers.contains(value)) {\n\t\t\t\t\t\tservers.add(value);\n\t\t\t\t\t}\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tWifiManager wMgr = (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE);\n\t\t\tif (wMgr != null) {\n\t\t\t\tDhcpInfo dhcp = wMgr.getDhcpInfo();\n\t\t\t\tif (dhcp.dns1 != 0) {\n\t\t\t\t\tString value = intToInetString(dhcp.dns1);\n\t\t\t\t\tif (value != null && !value.isEmpty() && !servers.contains(value)) {\n\t\t\t\t\t\tservers.add(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (dhcp.dns2 != 0) {\n\t\t\t\t\tString value = intToInetString(dhcp.dns2);\n\t\t\t\t\tif (value != null && !value.isEmpty() && !servers.contains(value)) {\n\t\t\t\t\t\tservers.add(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn String.join(\" \", servers);\n\t\tcase ConnectivityManager.TYPE_MOBILE:\n\t\tcase ConnectivityManager.TYPE_MOBILE_HIPRI:\n\t\t\tfor (String name : new String[]{\n\t\t\t\t\"net.rmnet0.dns1\", \"net.rmnet0.dns2\", \"net.rmnet0.dns3\", \"net.rmnet0.dns4\",\n\t\t\t\t\"net.rmnet1.dns1\", \"net.rmnet1.dns2\", \"net.rmnet1.dns3\", \"net.rmnet1.dns4\",\n\t\t\t\t\"net.rmnet2.dns1\", \"net.rmnet2.dns2\", \"net.rmnet2.dns3\", \"net.rmnet2.dns4\",\n\t\t\t\t\"net.rmnet3.dns1\", \"net.rmnet3.dns2\", \"net.rmnet3.dns3\", \"net.rmnet3.dns4\",\n\t\t\t\t\"net.rmnet4.dns1\", \"net.rmnet4.dns2\", \"net.rmnet4.dns3\", \"net.rmnet4.dns4\",\n\t\t\t\t\"net.rmnet5.dns1\", \"net.rmnet5.dns2\", \"net.rmnet5.dns3\", \"net.rmnet5.dns4\",\n\t\t\t\t\"net.rmnet6.dns1\", \"net.rmnet6.dns2\", \"net.rmnet6.dns3\", \"net.rmnet6.dns4\",\n\t\t\t\t\"net.rmnet7.dns1\", \"net.rmnet7.dns2\", \"net.rmnet7.dns3\", \"net.rmnet7.dns4\",\n\t\t\t\t\"net.pdp0.dns1\", \"net.pdp0.dns2\", \"net.pdp0.dns3\", \"net.pdp0.dns4\",\n\t\t\t\t\"net.pdpbr0.dns1\", \"net.pdpbr0.dns2\", \"net.pdpbr0.dns3\", \"net.pdpbr0.dns4\"}) {\n\t\t\t\ttry {\n\t\t\t\t\tString value = (String) method.invoke(null, name);\n\t\t\t\t\tif (value != null && !value.isEmpty() && !servers.contains(value)) {\n\t\t\t\t\t\tservers.add(value);\n\t\t\t\t\t}\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\n\t\treturn \"\";\n\t}\n\n\t// getPreferabilityForNetwork is a utility routine which implements a priority for\n\t// different types of network transport, used in a heuristic to pick DNS servers to use.\n\tint getPreferabilityForNetwork(ConnectivityManager cMgr, Network network) {\n\t\tNetworkCapabilities nc = cMgr.getNetworkCapabilities(network);\n\n\t\tif (nc == null) {\n\t\t\treturn -1;\n\t\t}\n\t\tif (nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {\n\t\t\t// tun0 has both VPN and WIFI set, have to check VPN first and return.\n\t\t\treturn -1;\n\t\t}\n\n\t\tif (nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {\n\t\t\treturn 0;\n\t\t} else if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {\n\t\t\treturn 1;\n\t\t} else if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {\n\t\t\treturn 2;\n\t\t} else {\n\t\t\treturn 3;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "android/src/main/java/com/tailscale/ipn/IPNActivity.java",
    "content": "// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage com.tailscale.ipn;\n\nimport android.app.Activity;\nimport android.content.res.AssetFileDescriptor;\nimport android.content.res.Configuration;\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.os.Bundle;\nimport android.provider.OpenableColumns;\nimport android.net.Uri;\nimport android.content.pm.PackageManager;\n\nimport java.util.List;\nimport java.util.ArrayList;\n\nimport org.gioui.GioView;\n\npublic final class IPNActivity extends Activity {\n\tfinal static int WRITE_STORAGE_RESULT = 1000;\n\n\tprivate GioView view;\n\n\t@Override public void onCreate(Bundle state) {\n\t\tsuper.onCreate(state);\n\t\tview = new GioView(this);\n\t\tsetContentView(view);\n\t\thandleIntent();\n\t}\n\n\t@Override public void onNewIntent(Intent i) {\n\t\tsetIntent(i);\n\t\thandleIntent();\n\t}\n\n\tprivate void handleIntent() {\n\t\tIntent it = getIntent();\n\t\tString act = it.getAction();\n\t\tString[] texts;\n\t\tUri[] uris;\n\t\tif (Intent.ACTION_SEND.equals(act)) {\n\t\t\turis = new Uri[]{it.getParcelableExtra(Intent.EXTRA_STREAM)};\n\t\t\ttexts = new String[]{it.getStringExtra(Intent.EXTRA_TEXT)};\n\t\t} else if (Intent.ACTION_SEND_MULTIPLE.equals(act)) {\n\t\t\tList<Uri> extraUris = it.getParcelableArrayListExtra(Intent.EXTRA_STREAM);\n\t\t\turis = extraUris.toArray(new Uri[0]);\n\t\t\ttexts = new String[uris.length];\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\tString mime = it.getType();\n\t\tint nitems = uris.length;\n\t\tString[] items = new String[nitems];\n\t\tString[] mimes = new String[nitems];\n\t\tint[] types = new int[nitems];\n\t\tString[] names = new String[nitems];\n\t\tlong[] sizes = new long[nitems];\n\t\tint nfiles = 0;\n\t\tfor (int i = 0; i < uris.length; i++) {\n\t\t\tString text = texts[i];\n\t\t\tUri uri = uris[i];\n\t\t\tif (text != null) {\n\t\t\t\ttypes[nfiles] = 1; // FileTypeText\n\t\t\t\tnames[nfiles] = \"file.txt\";\n\t\t\t\tmimes[nfiles] = mime;\n\t\t\t\titems[nfiles] = text;\n\t\t\t\t// Determined by len(text) in Go to eliminate UTF-8 encoding differences.\n\t\t\t\tsizes[nfiles] = 0;\n\t\t\t\tnfiles++;\n\t\t\t} else if (uri != null) {\n\t\t\t\tCursor c = getContentResolver().query(uri, null, null, null, null);\n\t\t\t\tif (c == null) {\n\t\t\t\t\t// Ignore files we have no permission to access.\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tint nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);\n\t\t\t\tint sizeCol = c.getColumnIndex(OpenableColumns.SIZE);\n\t\t\t\tc.moveToFirst();\n\t\t\t\tString name = c.getString(nameCol);\n\t\t\t\tlong size = c.getLong(sizeCol);\n\t\t\t\ttypes[nfiles] = 2; // FileTypeURI\n\t\t\t\tmimes[nfiles] = mime;\n\t\t\t\titems[nfiles] = uri.toString();\n\t\t\t\tnames[nfiles] = name;\n\t\t\t\tsizes[nfiles] = size;\n\t\t\t\tnfiles++;\n\t\t\t}\n\t\t}\n\t\tApp.onShareIntent(nfiles, types, mimes, items, names, sizes);\n\t}\n\n\t@Override public void onRequestPermissionsResult(int reqCode, String[] perms, int[] grants) {\n\t\tswitch (reqCode) {\n\t\tcase WRITE_STORAGE_RESULT:\n\t\t\tif (grants.length > 0 && grants[0] == PackageManager.PERMISSION_GRANTED) {\n\t\t\t\tApp.onWriteStorageGranted();\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override public void onDestroy() {\n\t\tview.destroy();\n\t\tsuper.onDestroy();\n\t}\n\n\t@Override public void onStart() {\n\t\tsuper.onStart();\n\t\tview.start();\n\t}\n\n\t@Override public void onStop() {\n\t\tview.stop();\n\t\tsuper.onStop();\n\t}\n\n\t@Override public void onConfigurationChanged(Configuration c) {\n\t\tsuper.onConfigurationChanged(c);\n\t\tview.configurationChanged();\n\t}\n\n\t@Override public void onLowMemory() {\n\t\tsuper.onLowMemory();\n\t\tview.onLowMemory();\n\t}\n\n\t@Override public void onBackPressed() {\n\t\tif (!view.backPressed())\n\t\t\tsuper.onBackPressed();\n\t}\n}\n"
  },
  {
    "path": "android/src/main/java/com/tailscale/ipn/IPNService.java",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage com.tailscale.ipn;\n\nimport android.os.Build;\nimport android.app.PendingIntent;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.net.VpnService;\nimport android.system.OsConstants;\n\nimport org.gioui.GioActivity;\n\nimport androidx.core.app.NotificationCompat;\nimport androidx.core.app.NotificationManagerCompat;\n\npublic class IPNService extends VpnService {\n\tpublic static final String ACTION_CONNECT = \"com.tailscale.ipn.CONNECT\";\n\tpublic static final String ACTION_DISCONNECT = \"com.tailscale.ipn.DISCONNECT\";\n\n\t@Override public int onStartCommand(Intent intent, int flags, int startId) {\n\t\tif (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) {\n\t\t\tclose();\n\t\t\treturn START_NOT_STICKY;\n\t\t}\n\t\tconnect();\n\t\treturn START_STICKY;\n\t}\n\n\tprivate void close() {\n\t\tstopForeground(true);\n\t\tdisconnect();\n\t}\n\n\t@Override public void onDestroy() {\n\t\tclose();\n\t\tsuper.onDestroy();\n\t}\n\n\t@Override public void onRevoke() {\n\t\tclose();\n\t\tsuper.onRevoke();\n\t}\n\n\tprivate PendingIntent configIntent() {\n\t\treturn PendingIntent.getActivity(this, 0, new Intent(this, IPNActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);\n\t}\n\n\tprivate void disallowApp(VpnService.Builder b, String name) {\n\t\ttry {\n\t\t\tb.addDisallowedApplication(name);\n\t\t} catch (PackageManager.NameNotFoundException e) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprotected VpnService.Builder newBuilder() {\n\t\tVpnService.Builder b = new VpnService.Builder()\n\t\t\t.setConfigureIntent(configIntent())\n\t\t\t.allowFamily(OsConstants.AF_INET)\n\t\t\t.allowFamily(OsConstants.AF_INET6);\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)\n\t\t\tb.setMetered(false); // Inherit the metered status from the underlying networks.\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)\n\t\t\tb.setUnderlyingNetworks(null); // Use all available networks.\n\n\t\t// RCS/Jibe https://github.com/tailscale/tailscale/issues/2322\n\t\tthis.disallowApp(b, \"com.google.android.apps.messaging\");\n\n\t\t// Stadia https://github.com/tailscale/tailscale/issues/3460\n\t\tthis.disallowApp(b, \"com.google.stadia.android\");\n\n\t\t// Android Auto https://github.com/tailscale/tailscale/issues/3828\n\t\tthis.disallowApp(b, \"com.google.android.projection.gearhead\");\n\n\t\treturn b;\n\t}\n\n\tpublic void notify(String title, String message) {\n\t\tNotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID)\n\t\t\t.setSmallIcon(R.drawable.ic_notification)\n\t\t\t.setContentTitle(title)\n\t\t\t.setContentText(message)\n\t\t\t.setContentIntent(configIntent())\n\t\t\t.setAutoCancel(true)\n\t\t\t.setOnlyAlertOnce(true)\n\t\t\t.setPriority(NotificationCompat.PRIORITY_DEFAULT);\n\n\t\tNotificationManagerCompat nm = NotificationManagerCompat.from(this);\n\t\tnm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build());\n\t}\n\n\tpublic void updateStatusNotification(String title, String message) {\n\t\tNotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID)\n\t\t\t.setSmallIcon(R.drawable.ic_notification)\n\t\t\t.setContentTitle(title)\n\t\t\t.setContentText(message)\n\t\t\t.setContentIntent(configIntent())\n\t\t\t.setPriority(NotificationCompat.PRIORITY_LOW);\n\n\t\tstartForeground(App.STATUS_NOTIFICATION_ID, builder.build());\n\t}\n\n\tprivate native void connect();\n\tprivate native void disconnect();\n}\n"
  },
  {
    "path": "android/src/main/java/com/tailscale/ipn/Peer.java",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage com.tailscale.ipn;\n\nimport android.app.Activity;\nimport android.app.Fragment;\nimport android.content.Intent;\n\npublic class Peer extends Fragment {\n\t@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {\n\t\tonActivityResult0(getActivity(), requestCode, resultCode);\n\t}\n\n\tprivate static native void onActivityResult0(Activity act, int reqCode, int resCode);\n}\n"
  },
  {
    "path": "android/src/main/java/com/tailscale/ipn/QuickToggleService.java",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage com.tailscale.ipn;\n\nimport android.content.Context;\nimport android.content.ComponentName;\nimport android.content.Intent;\nimport android.service.quicksettings.Tile;\nimport android.service.quicksettings.TileService;\n\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class QuickToggleService extends TileService {\n\t// lock protects the static fields below it.\n\tprivate static Object lock = new Object();\n\t// Active tracks whether the VPN is active.\n\tprivate static boolean active;\n\t// Ready tracks whether the tailscale backend is\n\t// ready to switch on/off.\n\tprivate static boolean ready;\n\t// currentTile tracks getQsTile while service is listening.\n\tprivate static Tile currentTile;\n\n\t@Override public void onStartListening() {\n\t\tsynchronized (lock) {\n\t\t\tcurrentTile = getQsTile();\n\t\t}\n\t\tupdateTile();\n\t}\n\n\t@Override public void onStopListening() {\n\t\tsynchronized (lock) {\n\t\t\tcurrentTile = null;\n\t\t}\n\t}\n\n\t@Override public void onClick() {\n\t\tboolean r;\n\t\tsynchronized (lock) {\n\t\t\tr = ready;\n\t\t}\n\t\tif (r) {\n\t\t\tonTileClick();\n\t\t} else {\n\t\t\t// Start main activity.\n\t\t\tIntent i = getPackageManager().getLaunchIntentForPackage(getPackageName());\n\t\t\tstartActivityAndCollapse(i);\n\t\t}\n\t}\n\n\tprivate static void updateTile() {\n\t\tTile t;\n\t\tboolean act;\n\t\tsynchronized (lock) {\n\t\t\tt = currentTile;\n\t\t\tact = active && ready;\n\t\t}\n\t\tif (t == null) {\n\t\t\treturn;\n\t\t}\n\t\tt.setState(act ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);\n\t\tt.updateTile();\n\t}\n\n\tstatic void setReady(Context ctx, boolean rdy) {\n\t\tsynchronized (lock) {\n\t\t\tready = rdy;\n\t\t}\n\t\tupdateTile();\n\t}\n\n\tstatic void setStatus(Context ctx, boolean act) {\n\t\tsynchronized (lock) {\n\t\t\tactive = act;\n\t\t}\n\t\tupdateTile();\n\t}\n\n\tprivate static native void onTileClick();\n}\n"
  },
  {
    "path": "android/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"100\"\n    android:viewportHeight=\"100\">\n  <group android:translateX=\"20\"\n      android:translateY=\"20\">\n    <path\n        android:pathData=\"M15,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M30,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M15,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M45,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M30,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M45,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M15,15.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M30,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M45,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "android/src/main/res/drawable/ic_tile.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"60\"\n    android:viewportHeight=\"60\">\n  <group>\n    <path\n        android:pathData=\"M15,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M30,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M15,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M45,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M30,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M45,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#FFFDFA\"/>\n    <path\n        android:pathData=\"M15,15.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M30,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n    <path\n        android:pathData=\"M45,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0\"\n        android:fillColor=\"#54514D\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#1F2125</color>\n</resources>"
  },
  {
    "path": "android/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">Tailscale</string>\n    <string name=\"tile_name\">Tailscale</string>\n</resources>\n"
  },
  {
    "path": "android/src/play/java/com/tailscale/ipn/Google.java",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage com.tailscale.ipn;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.content.Context;\n\nimport com.google.android.gms.auth.api.signin.GoogleSignIn;\nimport com.google.android.gms.auth.api.signin.GoogleSignInAccount;\nimport com.google.android.gms.auth.api.signin.GoogleSignInClient;\nimport com.google.android.gms.auth.api.signin.GoogleSignInOptions;\n\n// Google implements helpers for Google services.\npublic final class Google {\n\tstatic String getIdTokenForActivity(Activity act) {\n\t\tGoogleSignInAccount acc = GoogleSignIn.getLastSignedInAccount(act);\n\t\treturn acc.getIdToken();\n\t}\n\n\tstatic void googleSignIn(Activity act, String serverOAuthID, int reqCode) {\n\t\tact.runOnUiThread(new Runnable() {\n\t\t\t@Override public void run() {\n\t\t\t\tGoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)\n\t\t\t\t\t.requestIdToken(serverOAuthID)\n\t\t\t\t\t.requestEmail()\n\t\t\t\t\t.build();\n\t\t\t\tGoogleSignInClient client = GoogleSignIn.getClient(act, gso);\n\t\t\t\tIntent signInIntent = client.getSignInIntent();\n\t\t\t\tApp.startActivityForResult(act, signInIntent, reqCode);\n\t\t\t}\n\t\t});\n\t}\n\n\tstatic void googleSignOut(Context ctx) {\n\t\tGoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)\n\t\t\t.build();\n\t\tGoogleSignInClient client = GoogleSignIn.getClient(ctx, gso);\n\t\tclient.signOut();\n\t}\n}\n"
  },
  {
    "path": "android/src/test/java/com/tailscale/ipn/DnsConfigTest.java",
    "content": "import org.junit.Before;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\nimport com.tailscale.ipn.DnsConfig;\n\npublic class DnsConfigTest {\n\tDnsConfig dns;\n\n\t@Before\n\tpublic void setup() {\n\t\tdns = new DnsConfig(null);\n\t}\n\n\t@Test\n\tpublic void dnsConfig_intToInetStringTest() {\n\t\tassertEquals(dns.intToInetString(0x0101a8c0), \"192.168.1.1\");\n\t\tassertEquals(dns.intToInetString(0x04030201), \"1.2.3.4\");\n\t\tassertEquals(dns.intToInetString(0), \"0.0.0.0\");\n\t}\n}\n"
  },
  {
    "path": "cmd/tailscale/backend.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/tailscale/tailscale-android/jni\"\n\t\"golang.org/x/sys/unix\"\n\t\"golang.zx2c4.com/wireguard/tun\"\n\t\"inet.af/netaddr\"\n\t\"tailscale.com/ipn\"\n\t\"tailscale.com/ipn/ipnlocal\"\n\t\"tailscale.com/logpolicy\"\n\t\"tailscale.com/logtail\"\n\t\"tailscale.com/logtail/filch\"\n\t\"tailscale.com/net/dns\"\n\t\"tailscale.com/net/tsdial\"\n\t\"tailscale.com/smallzstd\"\n\t\"tailscale.com/types/logger\"\n\t\"tailscale.com/util/dnsname\"\n\t\"tailscale.com/wgengine\"\n\t\"tailscale.com/wgengine/netstack\"\n\t\"tailscale.com/wgengine/router\"\n)\n\ntype backend struct {\n\tengine     wgengine.Engine\n\tbackend    *ipnlocal.LocalBackend\n\tdevices    *multiTUN\n\tsettings   settingsFunc\n\tlastCfg    *router.Config\n\tlastDNSCfg *dns.OSConfig\n\n\tlogIDPublic string\n\n\t// avoidEmptyDNS controls whether to use fallback nameservers\n\t// when no nameservers are provided by Tailscale.\n\tavoidEmptyDNS bool\n\n\tjvm    *jni.JVM\n\tappCtx jni.Object\n}\n\ntype settingsFunc func(*router.Config, *dns.OSConfig) error\n\nconst defaultMTU = 1280 // minimalMTU from wgengine/userspace.go\n\nconst (\n\tlogPrefKey               = \"privatelogid\"\n\tloginMethodPrefKey       = \"loginmethod\"\n\tcustomLoginServerPrefKey = \"customloginserver\"\n)\n\nconst (\n\tloginMethodGoogle = \"google\"\n\tloginMethodWeb    = \"web\"\n)\n\n// googleDnsServers are used on ChromeOS, where an empty VpnBuilder DNS setting results\n// in erasing the platform DNS servers. The developer docs say this is not supposed to happen,\n// but nonetheless it does.\nvar googleDnsServers = []netaddr.IP{netaddr.MustParseIP(\"8.8.8.8\"), netaddr.MustParseIP(\"8.8.4.4\"),\n\tnetaddr.MustParseIP(\"2001:4860:4860::8888\"), netaddr.MustParseIP(\"2001:4860:4860::8844\"),\n}\n\n// errVPNNotPrepared is used when VPNService.Builder.establish returns\n// null, either because the VPNService is not yet prepared or because\n// VPN status was revoked.\nvar errVPNNotPrepared = errors.New(\"VPN service not prepared or was revoked\")\n\nfunc newBackend(dataDir string, jvm *jni.JVM, appCtx jni.Object, store *stateStore,\n\tsettings settingsFunc) (*backend, error) {\n\n\tlogf := logger.RusagePrefixLog(log.Printf)\n\tb := &backend{\n\t\tjvm:      jvm,\n\t\tdevices:  newTUNDevices(),\n\t\tsettings: settings,\n\t\tappCtx:   appCtx,\n\t}\n\tvar logID logtail.PrivateID\n\tlogID.UnmarshalText([]byte(\"dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000\"))\n\tstoredLogID, err := store.read(logPrefKey)\n\t// In all failure cases we ignore any errors and continue with the dead value above.\n\tif err != nil || storedLogID == nil {\n\t\t// Read failed or there was no previous log id.\n\t\tnewLogID, err := logtail.NewPrivateID()\n\t\tif err == nil {\n\t\t\tlogID = newLogID\n\t\t\tenc, err := newLogID.MarshalText()\n\t\t\tif err == nil {\n\t\t\t\tstore.write(logPrefKey, enc)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlogID.UnmarshalText([]byte(storedLogID))\n\t}\n\tb.SetupLogs(dataDir, logID)\n\tdialer := new(tsdial.Dialer)\n\tcb := &router.CallbackRouter{\n\t\tSetBoth:           b.setCfg,\n\t\tSplitDNS:          false,\n\t\tGetBaseConfigFunc: b.getDNSBaseConfig,\n\t}\n\tengine, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{\n\t\tTun:    b.devices,\n\t\tRouter: cb,\n\t\tDNS:    cb,\n\t\tDialer: dialer,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"runBackend: NewUserspaceEngine: %v\", err)\n\t}\n\tb.logIDPublic = logID.Public().String()\n\ttunDev, magicConn, dnsMgr, ok := engine.(wgengine.InternalsGetter).GetInternals()\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%T is not a wgengine.InternalsGetter\", engine)\n\t}\n\tns, err := netstack.Create(logf, tunDev, engine, magicConn, dialer, dnsMgr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"netstack.Create: %w\", err)\n\t}\n\tns.ProcessLocalIPs = false // let Android kernel handle it; VpnBuilder sets this up\n\tns.ProcessSubnets = true   // for Android-being-an-exit-node support\n\tlb, err := ipnlocal.NewLocalBackend(logf, b.logIDPublic, store, dialer, engine, 0)\n\tif err != nil {\n\t\tengine.Close()\n\t\treturn nil, fmt.Errorf(\"runBackend: NewLocalBackend: %v\", err)\n\t}\n\tns.SetLocalBackend(lb)\n\tif err := ns.Start(); err != nil {\n\t\treturn nil, fmt.Errorf(\"startNetstack: %w\", err)\n\t}\n\tb.engine = engine\n\tb.backend = lb\n\treturn b, nil\n}\n\nfunc (b *backend) Start(notify func(n ipn.Notify)) error {\n\tb.backend.SetNotifyCallback(notify)\n\treturn b.backend.Start(ipn.Options{\n\t\tStateKey: \"ipn-android\",\n\t})\n}\n\nfunc (b *backend) LinkChange() {\n\tif b.engine != nil {\n\t\tb.engine.LinkChange(false)\n\t}\n}\n\nfunc (b *backend) setCfg(rcfg *router.Config, dcfg *dns.OSConfig) error {\n\treturn b.settings(rcfg, dcfg)\n}\n\nfunc (b *backend) updateTUN(service jni.Object, rcfg *router.Config, dcfg *dns.OSConfig) error {\n\tif reflect.DeepEqual(rcfg, b.lastCfg) && reflect.DeepEqual(dcfg, b.lastDNSCfg) {\n\t\treturn nil\n\t}\n\n\t// Close previous tunnel(s).\n\t// This is necessary for ChromeOS, native Android devices\n\t// seem to handle seamless handover between tunnels correctly.\n\t//\n\t// TODO(eliasnaur): If seamless handover becomes a desirable feature, skip\n\t// the closing on ChromeOS.\n\tb.CloseTUNs()\n\n\tif len(rcfg.LocalAddrs) == 0 {\n\t\treturn nil\n\t}\n\terr := jni.Do(b.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, service)\n\t\t// Construct a VPNService.Builder. IPNService.newBuilder calls\n\t\t// setConfigureIntent, and allowFamily for both IPv4 and IPv6.\n\t\tm := jni.GetMethodID(env, cls, \"newBuilder\", \"()Landroid/net/VpnService$Builder;\")\n\t\tbuilder, err := jni.CallObjectMethod(env, service, m)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"IPNService.newBuilder: %v\", err)\n\t\t}\n\t\tbcls := jni.GetObjectClass(env, builder)\n\n\t\t// builder.setMtu.\n\t\tsetMtu := jni.GetMethodID(env, bcls, \"setMtu\", \"(I)Landroid/net/VpnService$Builder;\")\n\t\tconst mtu = defaultMTU\n\t\tif _, err := jni.CallObjectMethod(env, builder, setMtu, jni.Value(mtu)); err != nil {\n\t\t\treturn fmt.Errorf(\"VpnService.Builder.setMtu: %v\", err)\n\t\t}\n\n\t\t// builder.addDnsServer\n\t\taddDnsServer := jni.GetMethodID(env, bcls, \"addDnsServer\", \"(Ljava/lang/String;)Landroid/net/VpnService$Builder;\")\n\t\t// builder.addSearchDomain.\n\t\taddSearchDomain := jni.GetMethodID(env, bcls, \"addSearchDomain\", \"(Ljava/lang/String;)Landroid/net/VpnService$Builder;\")\n\t\tif dcfg != nil {\n\t\t\tnameservers := dcfg.Nameservers\n\t\t\tif b.avoidEmptyDNS && len(nameservers) == 0 {\n\t\t\t\tnameservers = googleDnsServers\n\t\t\t}\n\t\t\tfor _, dns := range nameservers {\n\t\t\t\t_, err = jni.CallObjectMethod(env,\n\t\t\t\t\tbuilder,\n\t\t\t\t\taddDnsServer,\n\t\t\t\t\tjni.Value(jni.JavaString(env, dns.String())),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"VpnService.Builder.addDnsServer(%v): %v\", dns, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor _, dom := range dcfg.SearchDomains {\n\t\t\t\t_, err = jni.CallObjectMethod(env,\n\t\t\t\t\tbuilder,\n\t\t\t\t\taddSearchDomain,\n\t\t\t\t\tjni.Value(jni.JavaString(env, dom.WithoutTrailingDot())),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"VpnService.Builder.addSearchDomain(%v): %v\", dom, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// builder.addRoute.\n\t\taddRoute := jni.GetMethodID(env, bcls, \"addRoute\", \"(Ljava/lang/String;I)Landroid/net/VpnService$Builder;\")\n\t\tfor _, route := range rcfg.Routes {\n\t\t\t// Normalize route address; Builder.addRoute does not accept non-zero masked bits.\n\t\t\troute = route.Masked()\n\t\t\t_, err = jni.CallObjectMethod(env,\n\t\t\t\tbuilder,\n\t\t\t\taddRoute,\n\t\t\t\tjni.Value(jni.JavaString(env, route.IP().String())),\n\t\t\t\tjni.Value(route.Bits()),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"VpnService.Builder.addRoute(%v): %v\", route, err)\n\t\t\t}\n\t\t}\n\n\t\t// builder.addAddress.\n\t\taddAddress := jni.GetMethodID(env, bcls, \"addAddress\", \"(Ljava/lang/String;I)Landroid/net/VpnService$Builder;\")\n\t\tfor _, addr := range rcfg.LocalAddrs {\n\t\t\t_, err = jni.CallObjectMethod(env,\n\t\t\t\tbuilder,\n\t\t\t\taddAddress,\n\t\t\t\tjni.Value(jni.JavaString(env, addr.IP().String())),\n\t\t\t\tjni.Value(addr.Bits()),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"VpnService.Builder.addAddress(%v): %v\", addr, err)\n\t\t\t}\n\t\t}\n\n\t\t// builder.establish.\n\t\testablish := jni.GetMethodID(env, bcls, \"establish\", \"()Landroid/os/ParcelFileDescriptor;\")\n\t\tparcelFD, err := jni.CallObjectMethod(env, builder, establish)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"VpnService.Builder.establish: %v\", err)\n\t\t}\n\t\tif parcelFD == 0 {\n\t\t\treturn errVPNNotPrepared\n\t\t}\n\n\t\t// detachFd.\n\t\tparcelCls := jni.GetObjectClass(env, parcelFD)\n\t\tdetachFd := jni.GetMethodID(env, parcelCls, \"detachFd\", \"()I\")\n\t\ttunFD, err := jni.CallIntMethod(env, parcelFD, detachFd)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"detachFd: %v\", err)\n\t\t}\n\n\t\t// Create TUN device.\n\t\ttunDev, _, err := tun.CreateUnmonitoredTUNFromFD(int(tunFD))\n\t\tif err != nil {\n\t\t\tunix.Close(int(tunFD))\n\t\t\treturn err\n\t\t}\n\n\t\tb.devices.add(tunDev)\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tb.lastCfg = nil\n\t\tb.CloseTUNs()\n\t\treturn err\n\t}\n\tb.lastCfg = rcfg\n\tb.lastDNSCfg = dcfg\n\treturn nil\n}\n\n// CloseVPN closes any active TUN devices.\nfunc (b *backend) CloseTUNs() {\n\tb.lastCfg = nil\n\tb.devices.Shutdown()\n}\n\n// SetupLogs sets up remote logging.\nfunc (b *backend) SetupLogs(logDir string, logID logtail.PrivateID) {\n\tlogcfg := logtail.Config{\n\t\tCollection: \"tailnode.log.tailscale.io\",\n\t\tPrivateID:  logID,\n\t\tStderr:     log.Writer(),\n\t\tNewZstdEncoder: func() logtail.Encoder {\n\t\t\tw, err := smallzstd.NewEncoder(nil)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\treturn w\n\t\t},\n\t\tHTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost)},\n\t}\n\tdrainCh := make(chan struct{})\n\tlogcfg.DrainLogs = drainCh\n\tgo func() {\n\t\t// Upload logs infrequently. Interval chosen arbitrarily.\n\t\t// The objective is to reduce phone power use.\n\t\tt := time.NewTicker(2 * time.Minute)\n\t\tfor range t.C {\n\t\t\tselect {\n\t\t\tcase drainCh <- struct{}{}:\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t}()\n\n\tfilchOpts := filch.Options{\n\t\tReplaceStderr: true,\n\t}\n\n\tvar filchErr error\n\tif logDir != \"\" {\n\t\tlogPath := filepath.Join(logDir, \"ipn.log.\")\n\t\tlogcfg.Buffer, filchErr = filch.New(logPath, filchOpts)\n\t}\n\n\tlogf := logger.RusagePrefixLog(log.Printf)\n\ttlog := logtail.NewLogger(logcfg, logf)\n\n\tlog.SetFlags(0)\n\tlog.SetOutput(tlog)\n\n\tlog.Printf(\"goSetupLogs: success\")\n\n\tif logDir == \"\" {\n\t\tlog.Printf(\"SetupLogs: no logDir, storing logs in memory\")\n\t}\n\tif filchErr != nil {\n\t\tlog.Printf(\"SetupLogs: filch setup failed: %v\", filchErr)\n\t}\n}\n\n// We log the result of each of the DNS configuration discovery mechanisms, as we're\n// expecting a long tail of obscure Android devices with interesting behavior.\nfunc (b *backend) logDNSConfigMechanisms() {\n\terr := jni.Do(b.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, b.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"getDnsConfigObj\", \"()Lcom/tailscale/ipn/DnsConfig;\")\n\t\tdns, err := jni.CallObjectMethod(env, b.appCtx, m)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"getDnsConfigObj JNI: %v\", err)\n\t\t}\n\t\tdnsCls := jni.GetObjectClass(env, dns)\n\n\t\tfor _, impl := range []string{\"getDnsConfigFromLinkProperties\",\n\t\t\t\"getDnsServersFromSystemProperties\",\n\t\t\t\"getDnsServersFromNetworkInfo\"} {\n\n\t\t\tm = jni.GetMethodID(env, dnsCls, impl, \"()Ljava/lang/String;\")\n\t\t\tn, err := jni.CallObjectMethod(env, dns, m)\n\t\t\tbaseConfig := jni.GoString(env, jni.String(n))\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"%s JNI: %v\", impl, err)\n\t\t\t} else {\n\t\t\t\toneLine := strings.Replace(baseConfig, \"\\n\", \";\", -1)\n\t\t\t\tlog.Printf(\"%s: %s\", impl, oneLine)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlog.Printf(\"logDNSConfigMechanisms: %v\", err)\n\t}\n}\n\nfunc (b *backend) getPlatformDNSConfig() string {\n\tvar baseConfig string\n\terr := jni.Do(b.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, b.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"getDnsConfigObj\", \"()Lcom/tailscale/ipn/DnsConfig;\")\n\t\tdns, err := jni.CallObjectMethod(env, b.appCtx, m)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"getDnsConfigObj: %v\", err)\n\t\t}\n\t\tdnsCls := jni.GetObjectClass(env, dns)\n\t\tm = jni.GetMethodID(env, dnsCls, \"getDnsConfigAsString\", \"()Ljava/lang/String;\")\n\t\tn, err := jni.CallObjectMethod(env, dns, m)\n\t\tbaseConfig = jni.GoString(env, jni.String(n))\n\t\treturn err\n\t})\n\tif err != nil {\n\t\tlog.Printf(\"getPlatformDNSConfig JNI: %v\", err)\n\t\treturn \"\"\n\t}\n\treturn baseConfig\n}\n\nfunc (b *backend) getDNSBaseConfig() (dns.OSConfig, error) {\n\tb.logDNSConfigMechanisms()\n\tbaseConfig := b.getPlatformDNSConfig()\n\tlines := strings.Split(baseConfig, \"\\n\")\n\tif len(lines) == 0 {\n\t\treturn dns.OSConfig{}, nil\n\t}\n\n\tconfig := dns.OSConfig{}\n\taddrs := strings.Trim(lines[0], \" \\n\")\n\tfor _, addr := range strings.Split(addrs, \" \") {\n\t\tip, err := netaddr.ParseIP(addr)\n\t\tif err == nil {\n\t\t\tconfig.Nameservers = append(config.Nameservers, ip)\n\t\t}\n\t}\n\n\tif len(lines) > 1 {\n\t\tfor _, s := range strings.Split(strings.Trim(lines[1], \" \\n\"), \" \") {\n\t\t\tdomain, err := dnsname.ToFQDN(s)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"getDNSBaseConfig: unable to parse %q: %v\", s, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconfig.SearchDomains = append(config.SearchDomains, domain)\n\t\t}\n\t}\n\n\treturn config, nil\n}\n"
  },
  {
    "path": "cmd/tailscale/callbacks.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\n// JNI implementations of Java native callback methods.\n\nimport (\n\t\"unsafe\"\n\n\t\"github.com/tailscale/tailscale-android/jni\"\n)\n\n// #include <jni.h>\nimport \"C\"\n\nvar (\n\t// onVPNPrepared is notified when VpnService.prepare succeeds.\n\tonVPNPrepared = make(chan struct{}, 1)\n\t// onVPNClosed is notified when VpnService.prepare fails, or when\n\t// the a running VPN connection is closed.\n\tonVPNClosed = make(chan struct{}, 1)\n\t// onVPNRevoked is notified whenever the VPN service is revoked.\n\tonVPNRevoked = make(chan struct{}, 1)\n\n\t// onConnect receives global IPNService references when\n\t// a VPN connection is requested.\n\tonConnect = make(chan jni.Object)\n\t// onDisconnect receives global IPNService references when\n\t// disconnecting.\n\tonDisconnect = make(chan jni.Object)\n\t// onConnectivityChange is notified every time the network\n\t// conditions change.\n\tonConnectivityChange = make(chan bool, 1)\n\n\t// onGoogleToken receives google ID tokens.\n\tonGoogleToken = make(chan string)\n\n\t// onFileShare receives file sharing intents.\n\tonFileShare = make(chan []File, 1)\n\n\t// onWriteStorageGranted is notified when we are granted WRITE_STORAGE_PERMISSION.\n\tonWriteStorageGranted = make(chan struct{}, 1)\n)\n\nconst (\n\t// Request codes for Android callbacks.\n\t// requestSignin is for Google Sign-In.\n\trequestSignin C.jint = 1000 + iota\n\t// requestPrepareVPN is for when Android's VpnService.prepare\n\t// completes.\n\trequestPrepareVPN\n)\n\n// resultOK is Android's Activity.RESULT_OK.\nconst resultOK = -1\n\n//export Java_com_tailscale_ipn_App_onVPNPrepared\nfunc Java_com_tailscale_ipn_App_onVPNPrepared(env *C.JNIEnv, class C.jclass) {\n\tnotifyVPNPrepared()\n}\n\n//export Java_com_tailscale_ipn_App_onWriteStorageGranted\nfunc Java_com_tailscale_ipn_App_onWriteStorageGranted(env *C.JNIEnv, class C.jclass) {\n\tselect {\n\tcase onWriteStorageGranted <- struct{}{}:\n\tdefault:\n\t}\n}\n\nfunc notifyVPNPrepared() {\n\tselect {\n\tcase onVPNPrepared <- struct{}{}:\n\tdefault:\n\t}\n}\n\nfunc notifyVPNRevoked() {\n\tselect {\n\tcase onVPNRevoked <- struct{}{}:\n\tdefault:\n\t}\n}\n\nfunc notifyVPNClosed() {\n\tselect {\n\tcase onVPNClosed <- struct{}{}:\n\tdefault:\n\t}\n}\n\n//export Java_com_tailscale_ipn_IPNService_connect\nfunc Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) {\n\tjenv := (*jni.Env)(unsafe.Pointer(env))\n\tonConnect <- jni.NewGlobalRef(jenv, jni.Object(this))\n}\n\n//export Java_com_tailscale_ipn_IPNService_disconnect\nfunc Java_com_tailscale_ipn_IPNService_disconnect(env *C.JNIEnv, this C.jobject) {\n\tjenv := (*jni.Env)(unsafe.Pointer(env))\n\tonDisconnect <- jni.NewGlobalRef(jenv, jni.Object(this))\n}\n\n//export Java_com_tailscale_ipn_App_onConnectivityChanged\nfunc Java_com_tailscale_ipn_App_onConnectivityChanged(env *C.JNIEnv, cls C.jclass, connected C.jboolean) {\n\tselect {\n\tcase <-onConnectivityChange:\n\tdefault:\n\t}\n\tonConnectivityChange <- connected == C.JNI_TRUE\n}\n\n//export Java_com_tailscale_ipn_QuickToggleService_onTileClick\nfunc Java_com_tailscale_ipn_QuickToggleService_onTileClick(env *C.JNIEnv, cls C.jclass) {\n\trequestBackend(ToggleEvent{})\n}\n\n//export Java_com_tailscale_ipn_Peer_onActivityResult0\nfunc Java_com_tailscale_ipn_Peer_onActivityResult0(env *C.JNIEnv, cls C.jclass, act C.jobject, reqCode, resCode C.jint) {\n\tswitch reqCode {\n\tcase requestSignin:\n\t\tif resCode != resultOK {\n\t\t\tonGoogleToken <- \"\"\n\t\t\tbreak\n\t\t}\n\t\tjenv := (*jni.Env)(unsafe.Pointer(env))\n\t\tm := jni.GetStaticMethodID(jenv, googleClass,\n\t\t\t\"getIdTokenForActivity\", \"(Landroid/app/Activity;)Ljava/lang/String;\")\n\t\tidToken, err := jni.CallStaticObjectMethod(jenv, googleClass, m, jni.Value(act))\n\t\tif err != nil {\n\t\t\tfatalErr(err)\n\t\t\tbreak\n\t\t}\n\t\ttok := jni.GoString(jenv, jni.String(idToken))\n\t\tonGoogleToken <- tok\n\tcase requestPrepareVPN:\n\t\tif resCode == resultOK {\n\t\t\tnotifyVPNPrepared()\n\t\t} else {\n\t\t\tnotifyVPNClosed()\n\t\t\tnotifyVPNRevoked()\n\t\t}\n\t}\n}\n\n//export Java_com_tailscale_ipn_App_onShareIntent\nfunc 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) {\n\tconst (\n\t\ttypeNone   = 0\n\t\ttypeInline = 1\n\t\ttypeURI    = 2\n\t)\n\tjenv := (*jni.Env)(unsafe.Pointer(env))\n\ttypes := jni.GetIntArrayElements(jenv, jni.IntArray(jtypes))\n\tmimes := jni.GetStringArrayElements(jenv, jni.ObjectArray(jmimes))\n\titems := jni.GetStringArrayElements(jenv, jni.ObjectArray(jitems))\n\tnames := jni.GetStringArrayElements(jenv, jni.ObjectArray(jnames))\n\tsizes := jni.GetLongArrayElements(jenv, jni.LongArray(jsizes))\n\tvar files []File\n\tfor i := 0; i < int(nfiles); i++ {\n\t\tf := File{\n\t\t\tType:     FileType(types[i]),\n\t\t\tMIMEType: mimes[i],\n\t\t\tName:     names[i],\n\t\t}\n\t\tif f.Name == \"\" {\n\t\t\tf.Name = \"file.bin\"\n\t\t}\n\t\tswitch f.Type {\n\t\tcase FileTypeText:\n\t\t\tf.Text = items[i]\n\t\t\tf.Size = int64(len(f.Text))\n\t\tcase FileTypeURI:\n\t\t\tf.URI = items[i]\n\t\t\tf.Size = sizes[i]\n\t\tdefault:\n\t\t\tpanic(\"unknown file type\")\n\t\t}\n\t\tfiles = append(files, f)\n\t}\n\tselect {\n\tcase <-onFileShare:\n\tdefault:\n\t}\n\tonFileShare <- files\n}\n"
  },
  {
    "path": "cmd/tailscale/main.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"gioui.org/app\"\n\t\"gioui.org/io/system\"\n\t\"gioui.org/layout\"\n\t\"gioui.org/op\"\n\t\"inet.af/netaddr\"\n\n\t\"github.com/tailscale/tailscale-android/jni\"\n\t\"tailscale.com/client/tailscale/apitype\"\n\t\"tailscale.com/hostinfo\"\n\t\"tailscale.com/ipn\"\n\t\"tailscale.com/ipn/ipnlocal\"\n\t\"tailscale.com/net/dns\"\n\t\"tailscale.com/net/interfaces\"\n\t\"tailscale.com/net/netns\"\n\t\"tailscale.com/paths\"\n\t\"tailscale.com/tailcfg\"\n\t\"tailscale.com/types/netmap\"\n\t\"tailscale.com/wgengine/router\"\n)\n\ntype App struct {\n\tjvm *jni.JVM\n\t// appCtx is a global reference to the com.tailscale.ipn.App instance.\n\tappCtx jni.Object\n\n\tstore             *stateStore\n\tlogIDPublicAtomic atomic.Value // of string\n\n\t// netStates receives the most recent network state.\n\tnetStates chan BackendState\n\t// prefs receives new preferences from the backend.\n\tprefs chan *ipn.Prefs\n\t// browseURLs receives URLs when the backend wants to browse.\n\tbrowseURLs chan string\n\t// targetsLoaded receives lists of file targets.\n\ttargetsLoaded chan FileTargets\n\t// invalidates receives whenever the window should be refreshed.\n\tinvalidates chan struct{}\n}\n\nvar (\n\t// googleClass is a global reference to the com.tailscale.ipn.Google class.\n\tgoogleClass jni.Class\n)\n\ntype FileTargets struct {\n\tTargets []*apitype.FileTarget\n\tErr     error\n}\n\ntype File struct {\n\tType     FileType\n\tName     string\n\tSize     int64\n\tMIMEType string\n\t// URI of the file, valid if Type is FileTypeURI.\n\tURI string\n\t// Text is the content of the file, if Type is FileTypeText.\n\tText string\n}\n\n// FileSendInfo describes the state of an ongoing file send operation.\ntype FileSendInfo struct {\n\tState FileSendState\n\t// Progress tracks the progress of the transfer from 0.0 to 1.0. Valid\n\t// only when State is FileSendStarted.\n\tProgress float64\n}\n\ntype clientState struct {\n\tbrowseURL string\n\tbackend   BackendState\n\t// query is the search query, in lowercase.\n\tquery string\n\n\tPeers []UIPeer\n}\n\ntype FileType uint8\n\n// FileType constants are known to IPNActivity.java.\nconst (\n\tFileTypeText FileType = 1\n\tFileTypeURI  FileType = 2\n)\n\ntype ExitStatus uint8\n\nconst (\n\t// No exit node selected.\n\tExitNone ExitStatus = iota\n\t// Exit node selected and exists, but is offline or missing.\n\tExitOffline\n\t// Exit node selected and online.\n\tExitOnline\n)\n\ntype FileSendState uint8\n\nconst (\n\tFileSendNotStarted FileSendState = iota\n\tFileSendConnecting\n\tFileSendTransferring\n\tFileSendComplete\n\tFileSendFailed\n)\n\ntype Peer struct {\n\tLabel  string\n\tOnline bool\n\tID     tailcfg.StableNodeID\n}\n\ntype BackendState struct {\n\tPrefs        *ipn.Prefs\n\tState        ipn.State\n\tNetworkMap   *netmap.NetworkMap\n\tLostInternet bool\n\t// Exits are the peers that can act as exit node.\n\tExits []Peer\n\t// ExitState describes the state of our exit node.\n\tExitStatus ExitStatus\n\t// Exit is our current exit node, if any.\n\tExit Peer\n}\n\n// UIEvent is an event flowing from the UI to the backend.\ntype UIEvent interface{}\n\ntype RouteAllEvent struct {\n\tID tailcfg.StableNodeID\n}\n\ntype ConnectEvent struct {\n\tEnable bool\n}\n\ntype CopyEvent struct {\n\tText string\n}\n\ntype SearchEvent struct {\n\tQuery string\n}\n\ntype OAuth2Event struct {\n\tToken *tailcfg.Oauth2Token\n}\n\ntype FileSendEvent struct {\n\tTarget  *apitype.FileTarget\n\tContext context.Context\n\tUpdates func(FileSendInfo)\n}\n\ntype SetLoginServerEvent struct {\n\tURL string\n}\n\n// UIEvent types.\ntype (\n\tToggleEvent       struct{}\n\tReauthEvent       struct{}\n\tBugEvent          struct{}\n\tWebAuthEvent      struct{}\n\tGoogleAuthEvent   struct{}\n\tLogoutEvent       struct{}\n\tBeExitNodeEvent   bool\n\tExitAllowLANEvent bool\n)\n\n// serverOAuthID is the OAuth ID of the tailscale-android server, used\n// by GoogleSignInOptions.Builder.requestIdToken.\nconst serverOAuthID = \"744055068597-hv4opg0h7vskq1hv37nq3u26t8c15qk0.apps.googleusercontent.com\"\n\n// releaseCertFingerprint is the SHA-1 fingerprint of the Google Play Store signing key.\n// It is used to check whether the app is signed for release.\nconst releaseCertFingerprint = \"86:9D:11:8B:63:1E:F8:35:C6:D9:C2:66:53:BC:28:22:2F:B8:C1:AE\"\n\n// backendEvents receives events from the UI (Activity, Tile etc.) to the backend.\nvar backendEvents = make(chan UIEvent)\n\nfunc main() {\n\ta := &App{\n\t\tjvm:           (*jni.JVM)(unsafe.Pointer(app.JavaVM())),\n\t\tappCtx:        jni.Object(app.AppContext()),\n\t\tnetStates:     make(chan BackendState, 1),\n\t\tbrowseURLs:    make(chan string, 1),\n\t\tprefs:         make(chan *ipn.Prefs, 1),\n\t\ttargetsLoaded: make(chan FileTargets, 1),\n\t\tinvalidates:   make(chan struct{}, 1),\n\t}\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tloader := jni.ClassLoaderFor(env, a.appCtx)\n\t\tcl, err := jni.LoadClass(env, loader, \"com.tailscale.ipn.Google\")\n\t\tif err != nil {\n\t\t\t// Ignore load errors; the Google class is not included in F-Droid builds.\n\t\t\treturn nil\n\t\t}\n\t\tgoogleClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl)))\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n\ta.store = newStateStore(a.jvm, a.appCtx)\n\tinterfaces.RegisterInterfaceGetter(a.getInterfaces)\n\tgo func() {\n\t\tif err := a.runBackend(); err != nil {\n\t\t\tfatalErr(err)\n\t\t}\n\t}()\n\tgo func() {\n\t\tif err := a.runUI(); err != nil {\n\t\t\tfatalErr(err)\n\t\t}\n\t}()\n\tapp.Main()\n}\n\nfunc (a *App) runBackend() error {\n\tappDir, err := app.DataDir()\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n\tpaths.AppSharedDir.Store(appDir)\n\thostinfo.SetOSVersion(a.osVersion())\n\tif !googleSignInEnabled() {\n\t\thostinfo.SetPackage(\"nogoogle\")\n\t}\n\tdeviceModel := a.modelName()\n\tif a.isChromeOS() {\n\t\tdeviceModel = \"ChromeOS: \" + deviceModel\n\t}\n\thostinfo.SetDeviceModel(deviceModel)\n\n\ttype configPair struct {\n\t\trcfg *router.Config\n\t\tdcfg *dns.OSConfig\n\t}\n\tconfigs := make(chan configPair)\n\tconfigErrs := make(chan error)\n\tb, err := newBackend(appDir, a.jvm, a.appCtx, a.store, func(rcfg *router.Config, dcfg *dns.OSConfig) error {\n\t\tif rcfg == nil {\n\t\t\treturn nil\n\t\t}\n\t\tconfigs <- configPair{rcfg, dcfg}\n\t\treturn <-configErrs\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\ta.logIDPublicAtomic.Store(b.logIDPublic)\n\tdefer b.CloseTUNs()\n\n\t// Contrary to the documentation for VpnService.Builder.addDnsServer,\n\t// ChromeOS doesn't fall back to the underlying network nameservers if\n\t// we don't provide any.\n\tb.avoidEmptyDNS = a.isChromeOS()\n\n\tvar timer *time.Timer\n\tvar alarmChan <-chan time.Time\n\talarm := func(t *time.Timer) {\n\t\tif timer != nil {\n\t\t\ttimer.Stop()\n\t\t}\n\t\ttimer = t\n\t\tif timer != nil {\n\t\t\talarmChan = timer.C\n\t\t}\n\t}\n\tnotifications := make(chan ipn.Notify, 1)\n\tstartErr := make(chan error)\n\t// Start from a goroutine to avoid deadlock when Start\n\t// calls the callback.\n\tgo func() {\n\t\tstartErr <- b.Start(func(n ipn.Notify) {\n\t\t\tnotifications <- n\n\t\t})\n\t}()\n\tvar (\n\t\tcfg       configPair\n\t\tstate     BackendState\n\t\tservice   jni.Object // of IPNService\n\t\tsigningIn bool\n\t)\n\tvar (\n\t\twaitingFilesDone = make(chan struct{})\n\t\twaitingFiles     bool\n\t\tprocessingFiles  bool\n\t)\n\tprocessFiles := func() {\n\t\tif !waitingFiles || processingFiles {\n\t\t\treturn\n\t\t}\n\t\tprocessingFiles = true\n\t\twaitingFiles = false\n\t\tgo func() {\n\t\t\tif err := a.processWaitingFiles(b.backend); err != nil {\n\t\t\t\tlog.Printf(\"processWaitingFiles: %v\", err)\n\t\t\t}\n\t\t\twaitingFilesDone <- struct{}{}\n\t\t}()\n\t}\n\tfor {\n\t\tselect {\n\t\tcase err := <-startErr:\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase <-waitingFilesDone:\n\t\t\tprocessingFiles = false\n\t\t\tprocessFiles()\n\t\tcase s := <-configs:\n\t\t\tcfg = s\n\t\t\tif b == nil || service == 0 || cfg.rcfg == nil {\n\t\t\t\tconfigErrs <- nil\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tconfigErrs <- b.updateTUN(service, cfg.rcfg, cfg.dcfg)\n\t\tcase n := <-notifications:\n\t\t\texitWasOnline := state.ExitStatus == ExitOnline\n\t\t\tif p := n.Prefs; p != nil {\n\t\t\t\tfirst := state.Prefs == nil\n\t\t\t\tstate.Prefs = p.Clone()\n\t\t\t\tstate.updateExitNodes()\n\t\t\t\tif first {\n\t\t\t\t\tstate.Prefs.Hostname = a.hostname()\n\t\t\t\t\tgo b.backend.SetPrefs(state.Prefs)\n\t\t\t\t}\n\t\t\t\ta.setPrefs(state.Prefs)\n\t\t\t}\n\t\t\tif s := n.State; s != nil {\n\t\t\t\toldState := state.State\n\t\t\t\tstate.State = *s\n\t\t\t\tif service != 0 {\n\t\t\t\t\ta.updateNotification(service, state.State)\n\t\t\t\t}\n\t\t\t\tif service != 0 {\n\t\t\t\t\tif cfg.rcfg != nil && state.State >= ipn.Starting {\n\t\t\t\t\t\tif err := b.updateTUN(service, cfg.rcfg, cfg.dcfg); err != nil {\n\t\t\t\t\t\t\tlog.Printf(\"VPN update failed: %v\", err)\n\t\t\t\t\t\t\tnotifyVPNClosed()\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tb.CloseTUNs()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Stop VPN if we logged out.\n\t\t\t\tif oldState > ipn.Stopped && state.State <= ipn.Stopped {\n\t\t\t\t\tif err := a.callVoidMethod(a.appCtx, \"stopVPN\", \"()V\"); err != nil {\n\t\t\t\t\t\tfatalErr(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ta.notify(state)\n\t\t\t}\n\t\t\tif u := n.BrowseToURL; u != nil {\n\t\t\t\tsigningIn = false\n\t\t\t\ta.setURL(*u)\n\t\t\t}\n\t\t\tif m := n.NetMap; m != nil {\n\t\t\t\tstate.NetworkMap = m\n\t\t\t\tstate.updateExitNodes()\n\t\t\t\ta.notify(state)\n\t\t\t\tif service != 0 {\n\t\t\t\t\talarm(a.notifyExpiry(service, m.Expiry))\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Notify if a previously online exit is not longer online (or missing).\n\t\t\tif service != 0 && exitWasOnline && state.ExitStatus == ExitOffline {\n\t\t\t\ta.pushNotify(service, \"Connection Lost\", \"Your exit node is offline. Disable your exit node or contact your network admin for help.\")\n\t\t\t}\n\t\t\ttargets, err := b.backend.FileTargets()\n\t\t\tif err != nil {\n\t\t\t\t// Construct a user-visible error message.\n\t\t\t\tif b.backend.State() != ipn.Running {\n\t\t\t\t\terr = fmt.Errorf(\"Not connected to tailscale\")\n\t\t\t\t} else {\n\t\t\t\t\terr = fmt.Errorf(\"Failed to load device list\")\n\t\t\t\t}\n\t\t\t}\n\t\t\ta.targetsLoaded <- FileTargets{targets, err}\n\t\t\twaitingFiles = n.FilesWaiting != nil\n\t\t\tprocessFiles()\n\t\tcase <-onWriteStorageGranted:\n\t\t\tprocessFiles()\n\t\tcase <-alarmChan:\n\t\t\tif m := state.NetworkMap; m != nil && service != 0 {\n\t\t\t\talarm(a.notifyExpiry(service, m.Expiry))\n\t\t\t}\n\t\tcase e := <-backendEvents:\n\t\t\tswitch e := e.(type) {\n\t\t\tcase OAuth2Event:\n\t\t\t\tgo b.backend.Login(e.Token)\n\t\t\tcase ToggleEvent:\n\t\t\t\tstate.Prefs.WantRunning = !state.Prefs.WantRunning\n\t\t\t\tgo b.backend.SetPrefs(state.Prefs)\n\t\t\tcase BeExitNodeEvent:\n\t\t\t\tstate.Prefs.SetAdvertiseExitNode(bool(e))\n\t\t\t\tgo b.backend.SetPrefs(state.Prefs)\n\t\t\tcase ExitAllowLANEvent:\n\t\t\t\tstate.Prefs.ExitNodeAllowLANAccess = bool(e)\n\t\t\t\tgo b.backend.SetPrefs(state.Prefs)\n\t\t\tcase WebAuthEvent:\n\t\t\t\tif !signingIn {\n\t\t\t\t\tgo b.backend.StartLoginInteractive()\n\t\t\t\t\tsigningIn = true\n\t\t\t\t}\n\t\t\tcase SetLoginServerEvent:\n\t\t\t\tstate.Prefs.ControlURL = e.URL\n\t\t\t\tb.backend.SetPrefs(state.Prefs)\n\t\t\t\t// A hack to get around ipnlocal's inability to update\n\t\t\t\t// ControlURL after Start()... Can we re-init instead?\n\t\t\t\tos.Exit(0)\n\t\t\tcase LogoutEvent:\n\t\t\t\tgo b.backend.Logout()\n\t\t\tcase ConnectEvent:\n\t\t\t\tstate.Prefs.WantRunning = e.Enable\n\t\t\t\tgo b.backend.SetPrefs(state.Prefs)\n\t\t\tcase RouteAllEvent:\n\t\t\t\tstate.Prefs.ExitNodeID = e.ID\n\t\t\t\tgo b.backend.SetPrefs(state.Prefs)\n\t\t\t\tstate.updateExitNodes()\n\t\t\t\ta.notify(state)\n\t\t\t}\n\t\tcase s := <-onConnect:\n\t\t\tjni.Do(a.jvm, func(env *jni.Env) error {\n\t\t\t\tif jni.IsSameObject(env, s, service) {\n\t\t\t\t\t// We already have a reference.\n\t\t\t\t\tjni.DeleteGlobalRef(env, s)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tif service != 0 {\n\t\t\t\t\tjni.DeleteGlobalRef(env, service)\n\t\t\t\t}\n\t\t\t\tnetns.SetAndroidProtectFunc(func(fd int) error {\n\t\t\t\t\treturn jni.Do(a.jvm, func(env *jni.Env) error {\n\t\t\t\t\t\t// Call https://developer.android.com/reference/android/net/VpnService#protect(int)\n\t\t\t\t\t\t// to mark fd as a socket that should bypass the VPN and use the underlying network.\n\t\t\t\t\t\tcls := jni.GetObjectClass(env, s)\n\t\t\t\t\t\tm := jni.GetMethodID(env, cls, \"protect\", \"(I)Z\")\n\t\t\t\t\t\tok, err := jni.CallBooleanMethod(env, s, m, jni.Value(fd))\n\t\t\t\t\t\t// TODO(bradfitz): return an error back up to netns if this fails, once\n\t\t\t\t\t\t// we've had some experience with this and analyzed the logs over a wide\n\t\t\t\t\t\t// range of Android phones. For now we're being paranoid and conservative\n\t\t\t\t\t\t// and do the JNI call to protect best effort, only logging if it fails.\n\t\t\t\t\t\t// The risk of returning an error is that it breaks users on some Android\n\t\t\t\t\t\t// versions even when they're not using exit nodes. I'd rather the\n\t\t\t\t\t\t// relatively few number of exit node users file bug reports if Tailscale\n\t\t\t\t\t\t// doesn't work and then we can look for this log print.\n\t\t\t\t\t\tif err != nil || !ok {\n\t\t\t\t\t\t\tlog.Printf(\"[unexpected] VpnService.protect(%d) = %v, %v\", fd, ok, err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn nil // even on error. see big TODO above.\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\tservice = s\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\ta.updateNotification(service, state.State)\n\t\t\tif m := state.NetworkMap; m != nil {\n\t\t\t\talarm(a.notifyExpiry(service, m.Expiry))\n\t\t\t}\n\t\t\tif cfg.rcfg != nil && state.State >= ipn.Starting {\n\t\t\t\tif err := b.updateTUN(service, cfg.rcfg, cfg.dcfg); err != nil {\n\t\t\t\t\tlog.Printf(\"VPN update failed: %v\", err)\n\t\t\t\t\tnotifyVPNClosed()\n\t\t\t\t}\n\t\t\t}\n\t\tcase connected := <-onConnectivityChange:\n\t\t\tif state.LostInternet != !connected {\n\t\t\t\tlog.Printf(\"LostInternet state change: %v -> %v\", state.LostInternet, !connected)\n\t\t\t}\n\t\t\tstate.LostInternet = !connected\n\t\t\tif b != nil {\n\t\t\t\tgo b.LinkChange()\n\t\t\t}\n\t\t\ta.notify(state)\n\t\tcase s := <-onDisconnect:\n\t\t\tb.CloseTUNs()\n\t\t\tjni.Do(a.jvm, func(env *jni.Env) error {\n\t\t\t\tdefer jni.DeleteGlobalRef(env, s)\n\t\t\t\tif jni.IsSameObject(env, service, s) {\n\t\t\t\t\tnetns.SetAndroidProtectFunc(nil)\n\t\t\t\t\tjni.DeleteGlobalRef(env, service)\n\t\t\t\t\tservice = 0\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif state.State >= ipn.Starting {\n\t\t\t\tnotifyVPNClosed()\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (a *App) processWaitingFiles(b *ipnlocal.LocalBackend) error {\n\tfiles, err := b.WaitingFiles()\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar aerr error\n\tfor _, f := range files {\n\t\tif err := a.downloadFile(b, f); err != nil && aerr == nil {\n\t\t\taerr = err\n\t\t}\n\t}\n\treturn aerr\n}\n\nfunc (a *App) downloadFile(b *ipnlocal.LocalBackend, f apitype.WaitingFile) (cerr error) {\n\tin, _, err := b.OpenFile(f.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer in.Close()\n\text := filepath.Ext(f.Name)\n\tmimeType := mime.TypeByExtension(ext)\n\tvar mediaURI string\n\terr = jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tinsertMedia := jni.GetMethodID(env, cls, \"insertMedia\", \"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;\")\n\t\tjname := jni.JavaString(env, f.Name)\n\t\tjmime := jni.JavaString(env, mimeType)\n\t\turi, err := jni.CallObjectMethod(env, a.appCtx, insertMedia, jni.Value(jname), jni.Value(jmime))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmediaURI = jni.GoString(env, jni.String(uri))\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"insertMedia: %w\", err)\n\t}\n\tdeleteURI := func(uri string) error {\n\t\treturn jni.Do(a.jvm, func(env *jni.Env) error {\n\t\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\t\tm := jni.GetMethodID(env, cls, \"deleteUri\", \"(Ljava/lang/String;)V\")\n\t\t\tjuri := jni.JavaString(env, uri)\n\t\t\treturn jni.CallVoidMethod(env, a.appCtx, m, jni.Value(juri))\n\t\t})\n\t}\n\tout, err := a.openURI(mediaURI, \"w\")\n\tif err != nil {\n\t\tdeleteURI(mediaURI)\n\t\treturn fmt.Errorf(\"openUri: %w\", err)\n\t}\n\tif _, err := io.Copy(out, in); err != nil {\n\t\tdeleteURI(mediaURI)\n\t\treturn fmt.Errorf(\"copy: %w\", err)\n\t}\n\tif err := out.Close(); err != nil {\n\t\tdeleteURI(mediaURI)\n\t\treturn fmt.Errorf(\"close: %w\", err)\n\t}\n\tif err := a.notifyFile(mediaURI, f.Name); err != nil {\n\t\tfatalErr(err)\n\t}\n\treturn b.DeleteFile(f.Name)\n}\n\n// openURI calls a.appCtx.getContentResolver().openFileDescriptor on uri and\n// mode and returns the detached file descriptor.\nfunc (a *App) openURI(uri, mode string) (*os.File, error) {\n\tvar f *os.File\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\topenURI := jni.GetMethodID(env, cls, \"openUri\", \"(Ljava/lang/String;Ljava/lang/String;)I\")\n\t\tjuri := jni.JavaString(env, uri)\n\t\tjmode := jni.JavaString(env, mode)\n\t\tfd, err := jni.CallIntMethod(env, a.appCtx, openURI, jni.Value(juri), jni.Value(jmode))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf = os.NewFile(uintptr(fd), \"media-store\")\n\t\treturn nil\n\t})\n\treturn f, err\n}\n\nfunc (a *App) isChromeOS() bool {\n\tvar chromeOS bool\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"isChromeOS\", \"()Z\")\n\t\tb, err := jni.CallBooleanMethod(env, a.appCtx, m)\n\t\tchromeOS = b\n\t\treturn err\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn chromeOS\n}\n\nfunc (s *BackendState) updateExitNodes() {\n\ts.ExitStatus = ExitNone\n\tvar exitID tailcfg.StableNodeID\n\tif p := s.Prefs; p != nil {\n\t\texitID = p.ExitNodeID\n\t\tif exitID != \"\" {\n\t\t\ts.ExitStatus = ExitOffline\n\t\t}\n\t}\n\thasMyExit := exitID == \"\"\n\ts.Exits = nil\n\tvar peers []*tailcfg.Node\n\tif s.NetworkMap != nil {\n\t\tpeers = s.NetworkMap.Peers\n\t}\n\tfor _, p := range peers {\n\t\tcanRoute := false\n\t\tfor _, r := range p.AllowedIPs {\n\t\t\tif r == netaddr.MustParseIPPrefix(\"0.0.0.0/0\") || r == netaddr.MustParseIPPrefix(\"::/0\") {\n\t\t\t\tcanRoute = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tmyExit := p.StableID == exitID\n\t\thasMyExit = hasMyExit || myExit\n\t\texit := Peer{\n\t\t\tLabel:  p.DisplayName(true),\n\t\t\tOnline: canRoute,\n\t\t\tID:     p.StableID,\n\t\t}\n\t\tif myExit {\n\t\t\ts.Exit = exit\n\t\t\tif canRoute {\n\t\t\t\ts.ExitStatus = ExitOnline\n\t\t\t}\n\t\t}\n\t\tif canRoute || myExit {\n\t\t\ts.Exits = append(s.Exits, exit)\n\t\t}\n\t}\n\tsort.Slice(s.Exits, func(i, j int) bool {\n\t\treturn s.Exits[i].Label < s.Exits[j].Label\n\t})\n\tif !hasMyExit {\n\t\t// Insert node missing from netmap.\n\t\ts.Exit = Peer{Label: \"Unknown device\", ID: exitID}\n\t\ts.Exits = append([]Peer{s.Exit}, s.Exits...)\n\t}\n}\n\n// hostname builds a hostname from android.os.Build fields, in place of a\n// useless os.Hostname().\nfunc (a *App) hostname() string {\n\tvar hostname string\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tgetHostname := jni.GetMethodID(env, cls, \"getHostname\", \"()Ljava/lang/String;\")\n\t\tn, err := jni.CallObjectMethod(env, a.appCtx, getHostname)\n\t\thostname = jni.GoString(env, jni.String(n))\n\t\treturn err\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn hostname\n}\n\n// osVersion returns android.os.Build.VERSION.RELEASE. \" [nogoogle]\" is appended\n// if Google Play services are not compiled in.\nfunc (a *App) osVersion() string {\n\tvar version string\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"getOSVersion\", \"()Ljava/lang/String;\")\n\t\tn, err := jni.CallObjectMethod(env, a.appCtx, m)\n\t\tversion = jni.GoString(env, jni.String(n))\n\t\treturn err\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn version\n}\n\n// modelName return the MANUFACTURER + MODEL from\n// android.os.Build.\nfunc (a *App) modelName() string {\n\tvar model string\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"getModelName\", \"()Ljava/lang/String;\")\n\t\tn, err := jni.CallObjectMethod(env, a.appCtx, m)\n\t\tmodel = jni.GoString(env, jni.String(n))\n\t\treturn err\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn model\n}\n\nfunc googleSignInEnabled() bool {\n\treturn googleClass != 0\n}\n\n// updateNotification updates the foreground persistent status notification.\nfunc (a *App) updateNotification(service jni.Object, state ipn.State) error {\n\tvar msg, title string\n\tswitch state {\n\tcase ipn.Starting:\n\t\ttitle, msg = \"Connecting...\", \"\"\n\tcase ipn.Running:\n\t\ttitle, msg = \"Connected\", \"\"\n\tdefault:\n\t\treturn nil\n\t}\n\treturn jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, service)\n\t\tupdate := jni.GetMethodID(env, cls, \"updateStatusNotification\", \"(Ljava/lang/String;Ljava/lang/String;)V\")\n\t\tjtitle := jni.JavaString(env, title)\n\t\tjmessage := jni.JavaString(env, msg)\n\t\treturn jni.CallVoidMethod(env, service, update, jni.Value(jtitle), jni.Value(jmessage))\n\t})\n}\n\n// notifyExpiry notifies the user of imminent session expiry and\n// returns a new timer that triggers when the user should be notified\n// again.\nfunc (a *App) notifyExpiry(service jni.Object, expiry time.Time) *time.Timer {\n\tif expiry.IsZero() {\n\t\treturn nil\n\t}\n\td := time.Until(expiry)\n\tvar title string\n\tconst msg = \"Reauthenticate to maintain the connection to your network.\"\n\tvar t *time.Timer\n\tconst (\n\t\taday = 24 * time.Hour\n\t\tsoon = 5 * time.Minute\n\t)\n\tswitch {\n\tcase d <= 0:\n\t\ttitle = \"Your authentication has expired!\"\n\tcase d <= soon:\n\t\ttitle = \"Your authentication expires soon!\"\n\t\tt = time.NewTimer(d)\n\tcase d <= aday:\n\t\ttitle = \"Your authentication expires in a day.\"\n\t\tt = time.NewTimer(d - soon)\n\tdefault:\n\t\treturn time.NewTimer(d - aday)\n\t}\n\tif err := a.pushNotify(service, title, msg); err != nil {\n\t\tfatalErr(err)\n\t}\n\treturn t\n}\n\nfunc (a *App) notifyFile(uri, msg string) error {\n\treturn jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tnotify := jni.GetMethodID(env, cls, \"notifyFile\", \"(Ljava/lang/String;Ljava/lang/String;)V\")\n\t\tjuri := jni.JavaString(env, uri)\n\t\tjmsg := jni.JavaString(env, msg)\n\t\treturn jni.CallVoidMethod(env, a.appCtx, notify, jni.Value(juri), jni.Value(jmsg))\n\t})\n}\n\nfunc (a *App) pushNotify(service jni.Object, title, msg string) error {\n\treturn jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, service)\n\t\tnotify := jni.GetMethodID(env, cls, \"notify\", \"(Ljava/lang/String;Ljava/lang/String;)V\")\n\t\tjtitle := jni.JavaString(env, title)\n\t\tjmessage := jni.JavaString(env, msg)\n\t\treturn jni.CallVoidMethod(env, service, notify, jni.Value(jtitle), jni.Value(jmessage))\n\t})\n}\n\nfunc (a *App) notify(state BackendState) {\n\tselect {\n\tcase <-a.netStates:\n\tdefault:\n\t}\n\ta.netStates <- state\n\tready := jni.Bool(state.State >= ipn.Stopped)\n\tif err := a.callVoidMethod(a.appCtx, \"setTileReady\", \"(Z)V\", jni.Value(ready)); err != nil {\n\t\tfatalErr(err)\n\t}\n}\n\nfunc (a *App) setPrefs(prefs *ipn.Prefs) {\n\twantRunning := jni.Bool(prefs.WantRunning)\n\tif err := a.callVoidMethod(a.appCtx, \"setTileStatus\", \"(Z)V\", jni.Value(wantRunning)); err != nil {\n\t\tfatalErr(err)\n\t}\n\tselect {\n\tcase <-a.prefs:\n\tdefault:\n\t}\n\ta.prefs <- prefs\n}\n\nfunc (a *App) setURL(url string) {\n\tselect {\n\tcase <-a.browseURLs:\n\tdefault:\n\t}\n\ta.browseURLs <- url\n}\n\nfunc (a *App) runUI() error {\n\tw := app.NewWindow()\n\tui, err := newUI(a.store)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar ops op.Ops\n\tstate := new(clientState)\n\tvar (\n\t\t// activity is the most recent Android Activity reference as reported\n\t\t// by Gio ViewEvents.\n\t\tactivity jni.Object\n\t\t// files is list of files from the most recent file sharing intent.\n\t\tfiles []File\n\t)\n\tdeleteActivityRef := func() {\n\t\tif activity == 0 {\n\t\t\treturn\n\t\t}\n\t\tjni.Do(a.jvm, func(env *jni.Env) error {\n\t\t\tjni.DeleteGlobalRef(env, activity)\n\t\t\treturn nil\n\t\t})\n\t\tactivity = 0\n\t}\n\tdefer deleteActivityRef()\n\tfor {\n\t\tselect {\n\t\tcase <-onVPNClosed:\n\t\t\trequestBackend(ConnectEvent{Enable: false})\n\t\tcase tok := <-onGoogleToken:\n\t\t\tui.signinType = noSignin\n\t\t\tif tok != \"\" {\n\t\t\t\trequestBackend(OAuth2Event{\n\t\t\t\t\tToken: &tailcfg.Oauth2Token{\n\t\t\t\t\t\tAccessToken: tok,\n\t\t\t\t\t\tTokenType:   ipn.GoogleIDTokenType,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\t// Warn about possible debug certificate.\n\t\t\t\tif !a.isReleaseSigned() {\n\t\t\t\t\tui.ShowMessage(\"Google Sign-In failed because the app is not signed for Play Store\")\n\t\t\t\t\tw.Invalidate()\n\t\t\t\t}\n\t\t\t}\n\t\tcase p := <-a.prefs:\n\t\t\tui.enabled.Value = p.WantRunning\n\t\t\tui.runningExit = p.AdvertisesExitNode()\n\t\t\tui.exitLAN.Value = p.ExitNodeAllowLANAccess\n\t\t\tw.Invalidate()\n\t\tcase url := <-a.browseURLs:\n\t\t\tui.signinType = noSignin\n\t\t\tif a.isTV() {\n\t\t\t\tui.ShowQRCode(url)\n\t\t\t} else {\n\t\t\t\tstate.browseURL = url\n\t\t\t}\n\t\t\tw.Invalidate()\n\t\t\ta.updateState(activity, state)\n\t\tcase newState := <-a.netStates:\n\t\t\toldState := state.backend.State\n\t\t\tstate.backend = newState\n\t\t\ta.updateState(activity, state)\n\t\t\tw.Invalidate()\n\t\t\tif activity != 0 {\n\t\t\t\tnewState := state.backend.State\n\t\t\t\t// Start VPN if we just logged in.\n\t\t\t\tif oldState <= ipn.Stopped && newState > ipn.Stopped {\n\t\t\t\t\tif err := a.prepareVPN(activity); err != nil {\n\t\t\t\t\t\tfatalErr(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase <-onVPNPrepared:\n\t\t\tif state.backend.State > ipn.Stopped {\n\t\t\t\tif err := a.callVoidMethod(a.appCtx, \"startVPN\", \"()V\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif activity != 0 {\n\t\t\t\t\tif err := a.callVoidMethod(a.appCtx, \"requestWriteStoragePermission\", \"(Landroid/app/Activity;)V\", jni.Value(activity)); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase <-onVPNRevoked:\n\t\t\tui.ShowMessage(\"VPN access denied or another VPN service is always-on\")\n\t\t\tw.Invalidate()\n\t\tcase files = <-onFileShare:\n\t\t\tui.ShowShareDialog()\n\t\t\tw.Invalidate()\n\t\tcase t := <-a.targetsLoaded:\n\t\t\tui.FillShareDialog(t.Targets, t.Err)\n\t\t\tw.Invalidate()\n\t\tcase <-a.invalidates:\n\t\t\tw.Invalidate()\n\t\tcase e := <-w.Events():\n\t\t\tswitch e := e.(type) {\n\t\t\tcase app.ViewEvent:\n\t\t\t\tdeleteActivityRef()\n\t\t\t\tview := jni.Object(e.View)\n\t\t\t\tif view == 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tactivity = a.contextForView(view)\n\t\t\t\tw.Invalidate()\n\t\t\t\ta.attachPeer(activity)\n\t\t\t\tif state.backend.State > ipn.Stopped {\n\t\t\t\t\tif err := a.prepareVPN(activity); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase system.DestroyEvent:\n\t\t\t\treturn e.Err\n\t\t\tcase *system.CommandEvent:\n\t\t\t\tif e.Type == system.CommandBack {\n\t\t\t\t\tif ui.onBack() {\n\t\t\t\t\t\te.Cancel = true\n\t\t\t\t\t\tw.Invalidate()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase system.FrameEvent:\n\t\t\t\tins := e.Insets\n\t\t\t\te.Insets = system.Insets{}\n\t\t\t\tgtx := layout.NewContext(&ops, e)\n\t\t\t\tevents := ui.layout(gtx, ins, state)\n\t\t\t\te.Frame(gtx.Ops)\n\t\t\t\ta.processUIEvents(w, events, activity, state, files)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (a *App) isTV() bool {\n\tvar istv bool\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"isTV\", \"()Z\")\n\t\tb, err := jni.CallBooleanMethod(env, a.appCtx, m)\n\t\tistv = b\n\t\treturn err\n\t})\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n\treturn istv\n}\n\n// isReleaseSigned reports whether the app is signed with a release\n// signature.\nfunc (a *App) isReleaseSigned() bool {\n\tvar cert []byte\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"getPackageCertificate\", \"()[B\")\n\t\tstr, err := jni.CallObjectMethod(env, a.appCtx, m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert = jni.GetByteArrayElements(env, jni.ByteArray(str))\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n\th := sha1.New()\n\th.Write(cert)\n\tfingerprint := h.Sum(nil)\n\thex := fmt.Sprintf(\"%x\", fingerprint)\n\t// Strip colons and convert to lower case to ease comparing.\n\twantFingerprint := strings.ReplaceAll(strings.ToLower(releaseCertFingerprint), \":\", \"\")\n\treturn hex == wantFingerprint\n}\n\n// attachPeer registers an Android Fragment instance for\n// handling onActivityResult callbacks.\nfunc (a *App) attachPeer(act jni.Object) {\n\terr := a.callVoidMethod(a.appCtx, \"attachPeer\", \"(Landroid/app/Activity;)V\", jni.Value(act))\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n}\n\nfunc (a *App) updateState(act jni.Object, state *clientState) {\n\tif act != 0 && state.browseURL != \"\" {\n\t\ta.browseToURL(act, state.browseURL)\n\t\tstate.browseURL = \"\"\n\t}\n\n\tnetmap := state.backend.NetworkMap\n\tvar (\n\t\tpeers []*tailcfg.Node\n\t\tmyID  tailcfg.UserID\n\t)\n\tif netmap != nil {\n\t\tpeers = netmap.Peers\n\t\tmyID = netmap.User\n\t}\n\t// Split into sections.\n\tusers := make(map[tailcfg.UserID]struct{})\n\tvar uiPeers []UIPeer\n\tfor _, p := range peers {\n\t\tif q := state.query; q != \"\" {\n\t\t\t// Filter peers according to search query.\n\t\t\thost := strings.ToLower(p.Hostinfo.Hostname())\n\t\t\tname := strings.ToLower(p.Name)\n\t\t\tvar addr string\n\t\t\tif len(p.Addresses) > 0 {\n\t\t\t\taddr = p.Addresses[0].IP().String()\n\t\t\t}\n\t\t\tif !strings.Contains(host, q) && !strings.Contains(name, q) && !strings.Contains(addr, q) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tusers[p.User] = struct{}{}\n\t\tuiPeers = append(uiPeers, UIPeer{\n\t\t\tOwner: p.User,\n\t\t\tPeer:  p,\n\t\t})\n\t}\n\t// Add section (user) headers.\n\tfor u := range users {\n\t\tname := netmap.UserProfiles[u].DisplayName\n\t\tname = strings.ToUpper(name)\n\t\tuiPeers = append(uiPeers, UIPeer{Owner: u, Name: name})\n\t}\n\tsort.Slice(uiPeers, func(i, j int) bool {\n\t\tlhs, rhs := uiPeers[i], uiPeers[j]\n\t\tif lu, ru := lhs.Owner, rhs.Owner; ru != lu {\n\t\t\t// Sort own peers first.\n\t\t\tif lu == myID {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif ru == myID {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn lu < ru\n\t\t}\n\t\tlp, rp := lhs.Peer, rhs.Peer\n\t\t// Sort headers first.\n\t\tif lp == nil {\n\t\t\treturn true\n\t\t}\n\t\tif rp == nil {\n\t\t\treturn false\n\t\t}\n\t\tlName := lp.DisplayName(lp.User == myID)\n\t\trName := rp.DisplayName(rp.User == myID)\n\t\treturn lName < rName || lName == rName && lp.ID < rp.ID\n\t})\n\tstate.Peers = uiPeers\n}\n\nfunc (a *App) prepareVPN(act jni.Object) error {\n\treturn a.callVoidMethod(a.appCtx, \"prepareVPN\", \"(Landroid/app/Activity;I)V\",\n\t\tjni.Value(act), jni.Value(requestPrepareVPN))\n}\n\nfunc requestBackend(e UIEvent) {\n\tgo func() {\n\t\tbackendEvents <- e\n\t}()\n}\n\nfunc (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, state *clientState, files []File) {\n\tfor _, e := range events {\n\t\tswitch e := e.(type) {\n\t\tcase ReauthEvent:\n\t\t\tmethod, _ := a.store.ReadString(loginMethodPrefKey, loginMethodWeb)\n\t\t\tswitch method {\n\t\t\tcase loginMethodGoogle:\n\t\t\t\ta.googleSignIn(act)\n\t\t\tdefault:\n\t\t\t\trequestBackend(WebAuthEvent{})\n\t\t\t}\n\t\tcase BugEvent:\n\t\t\tbackendLogID, _ := a.logIDPublicAtomic.Load().(string)\n\t\t\tlogMarker := fmt.Sprintf(\"BUG-%v-%v-%v\", backendLogID, time.Now().UTC().Format(\"20060102150405Z\"), randHex(8))\n\t\t\tlog.Printf(\"user bugreport: %s\", logMarker)\n\t\t\tw.WriteClipboard(logMarker)\n\t\tcase BeExitNodeEvent:\n\t\t\trequestBackend(e)\n\t\tcase ExitAllowLANEvent:\n\t\t\trequestBackend(e)\n\t\tcase WebAuthEvent:\n\t\t\ta.store.WriteString(loginMethodPrefKey, loginMethodWeb)\n\t\t\trequestBackend(e)\n\t\tcase SetLoginServerEvent:\n\t\t\ta.store.WriteString(customLoginServerPrefKey, e.URL)\n\t\t\trequestBackend(e)\n\t\tcase LogoutEvent:\n\t\t\ta.signOut()\n\t\t\trequestBackend(e)\n\t\tcase ConnectEvent:\n\t\t\trequestBackend(e)\n\t\tcase RouteAllEvent:\n\t\t\trequestBackend(e)\n\t\tcase CopyEvent:\n\t\t\tw.WriteClipboard(e.Text)\n\t\tcase GoogleAuthEvent:\n\t\t\ta.store.WriteString(loginMethodPrefKey, loginMethodGoogle)\n\t\t\ta.googleSignIn(act)\n\t\tcase SearchEvent:\n\t\t\tstate.query = strings.ToLower(e.Query)\n\t\t\ta.updateState(act, state)\n\t\tcase FileSendEvent:\n\t\t\ta.sendFiles(e, files)\n\t\t}\n\t}\n}\n\nfunc (a *App) sendFiles(e FileSendEvent, files []File) {\n\tgo func() {\n\t\tvar totalSize int64\n\t\tfor _, f := range files {\n\t\t\ttotalSize += f.Size\n\t\t}\n\t\tif totalSize == 0 {\n\t\t\ttotalSize = 1\n\t\t}\n\t\tvar totalSent int64\n\t\tprogress := func(n int64) {\n\t\t\ttotalSent += n\n\t\t\te.Updates(FileSendInfo{\n\t\t\t\tState:    FileSendTransferring,\n\t\t\t\tProgress: float64(totalSent) / float64(totalSize),\n\t\t\t})\n\t\t\ta.invalidate()\n\t\t}\n\t\tdefer a.invalidate()\n\t\tfor _, f := range files {\n\t\t\tif err := a.sendFile(e.Context, e.Target, f, progress); err != nil {\n\t\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\te.Updates(FileSendInfo{\n\t\t\t\t\tState: FileSendFailed,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\te.Updates(FileSendInfo{\n\t\t\tState: FileSendComplete,\n\t\t})\n\t}()\n}\n\nfunc (a *App) invalidate() {\n\tselect {\n\tcase a.invalidates <- struct{}{}:\n\tdefault:\n\t}\n}\n\nfunc (a *App) sendFile(ctx context.Context, target *apitype.FileTarget, f File, progress func(n int64)) error {\n\tvar body io.Reader\n\tswitch f.Type {\n\tcase FileTypeText:\n\t\tbody = strings.NewReader(f.Text)\n\tcase FileTypeURI:\n\t\tf, err := a.openURI(f.URI, \"r\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer f.Close()\n\t\tbody = f\n\tdefault:\n\t\tpanic(\"unknown file type\")\n\t}\n\tbody = &progressReader{r: body, size: f.Size, progress: progress}\n\tdstURL := target.PeerAPIURL + \"/v0/put/\" + url.PathEscape(f.Name)\n\treq, err := http.NewRequestWithContext(ctx, \"PUT\", dstURL, body)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.ContentLength = f.Size\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"PUT failed: %s\", res.Status)\n\t}\n\treturn nil\n}\n\n// progressReader wraps an io.Reader to call a progress function\n// on every non-zero Read.\ntype progressReader struct {\n\tr        io.Reader\n\tbytes    int64\n\tsize     int64\n\teof      bool\n\tprogress func(n int64)\n}\n\nfunc (r *progressReader) Read(p []byte) (int, error) {\n\tn, err := r.r.Read(p)\n\t// The request body may be read after http.Client.Do returns, see\n\t// https://github.com/golang/go/issues/30597. Don't update progress if the\n\t// file has been read.\n\tr.eof = r.eof || errors.Is(err, io.EOF)\n\tif !r.eof && r.bytes < r.size {\n\t\tr.progress(int64(n))\n\t\tr.bytes += int64(n)\n\t}\n\treturn n, err\n}\n\nfunc (a *App) signOut() {\n\tif googleClass == 0 {\n\t\treturn\n\t}\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tm := jni.GetStaticMethodID(env, googleClass,\n\t\t\t\"googleSignOut\", \"(Landroid/content/Context;)V\")\n\t\treturn jni.CallStaticVoidMethod(env, googleClass, m, jni.Value(a.appCtx))\n\t})\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n}\n\nfunc (a *App) googleSignIn(act jni.Object) {\n\tif act == 0 || googleClass == 0 {\n\t\treturn\n\t}\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tsid := jni.JavaString(env, serverOAuthID)\n\t\tm := jni.GetStaticMethodID(env, googleClass,\n\t\t\t\"googleSignIn\", \"(Landroid/app/Activity;Ljava/lang/String;I)V\")\n\t\treturn jni.CallStaticVoidMethod(env, googleClass, m,\n\t\t\tjni.Value(act), jni.Value(sid), jni.Value(requestSignin))\n\t})\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n}\n\nfunc (a *App) browseToURL(act jni.Object, url string) {\n\tif act == 0 {\n\t\treturn\n\t}\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tjurl := jni.JavaString(env, url)\n\t\treturn a.callVoidMethod(a.appCtx, \"showURL\", \"(Landroid/app/Activity;Ljava/lang/String;)V\", jni.Value(act), jni.Value(jurl))\n\t})\n\tif err != nil {\n\t\tfatalErr(err)\n\t}\n}\n\nfunc (a *App) callVoidMethod(obj jni.Object, name, sig string, args ...jni.Value) error {\n\tif obj == 0 {\n\t\tpanic(\"invalid object\")\n\t}\n\treturn jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, obj)\n\t\tm := jni.GetMethodID(env, cls, name, sig)\n\t\treturn jni.CallVoidMethod(env, obj, m, args...)\n\t})\n}\n\n// activityForView calls View.getContext and returns a global\n// reference to the result.\nfunc (a *App) contextForView(view jni.Object) jni.Object {\n\tif view == 0 {\n\t\tpanic(\"invalid object\")\n\t}\n\tvar ctx jni.Object\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, view)\n\t\tm := jni.GetMethodID(env, cls, \"getContext\", \"()Landroid/content/Context;\")\n\t\tvar err error\n\t\tctx, err = jni.CallObjectMethod(env, view, m)\n\t\tctx = jni.NewGlobalRef(env, ctx)\n\t\treturn err\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn ctx\n}\n\n// Report interfaces in the device in net.Interface format.\nfunc (a *App) getInterfaces() ([]interfaces.Interface, error) {\n\tvar ifaceString string\n\terr := jni.Do(a.jvm, func(env *jni.Env) error {\n\t\tcls := jni.GetObjectClass(env, a.appCtx)\n\t\tm := jni.GetMethodID(env, cls, \"getInterfacesAsString\", \"()Ljava/lang/String;\")\n\t\tn, err := jni.CallObjectMethod(env, a.appCtx, m)\n\t\tifaceString = jni.GoString(env, jni.String(n))\n\t\treturn err\n\n\t})\n\tvar ifaces []interfaces.Interface\n\tif err != nil {\n\t\treturn ifaces, err\n\t}\n\n\tfor _, iface := range strings.Split(ifaceString, \"\\n\") {\n\t\t// Example of the strings we're processing:\n\t\t// wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24\n\t\t// r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64\n\t\t// mnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64\n\n\t\tif strings.TrimSpace(iface) == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tfields := strings.Split(iface, \"|\")\n\t\tif len(fields) != 2 {\n\t\t\tlog.Printf(\"getInterfaces: unable to split %q\", iface)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar name string\n\t\tvar index, mtu int\n\t\tvar up, broadcast, loopback, pointToPoint, multicast bool\n\t\t_, err := fmt.Sscanf(fields[0], \"%s %d %d %t %t %t %t %t\",\n\t\t\t&name, &index, &mtu, &up, &broadcast, &loopback, &pointToPoint, &multicast)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"getInterfaces: unable to parse %q: %v\", iface, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tnewIf := interfaces.Interface{\n\t\t\tInterface: &net.Interface{\n\t\t\t\tName:  name,\n\t\t\t\tIndex: index,\n\t\t\t\tMTU:   mtu,\n\t\t\t},\n\t\t\tAltAddrs: []net.Addr{}, // non-nil to avoid Go using netlink\n\t\t}\n\t\tif up {\n\t\t\tnewIf.Flags |= net.FlagUp\n\t\t}\n\t\tif broadcast {\n\t\t\tnewIf.Flags |= net.FlagBroadcast\n\t\t}\n\t\tif loopback {\n\t\t\tnewIf.Flags |= net.FlagLoopback\n\t\t}\n\t\tif pointToPoint {\n\t\t\tnewIf.Flags |= net.FlagPointToPoint\n\t\t}\n\t\tif multicast {\n\t\t\tnewIf.Flags |= net.FlagMulticast\n\t\t}\n\n\t\taddrs := strings.Trim(fields[1], \" \\n\")\n\t\tfor _, addr := range strings.Split(addrs, \" \") {\n\t\t\tip, err := netaddr.ParseIPPrefix(addr)\n\t\t\tif err == nil {\n\t\t\t\tnewIf.AltAddrs = append(newIf.AltAddrs, ip.IPNet())\n\t\t\t}\n\t\t}\n\n\t\tifaces = append(ifaces, newIf)\n\t}\n\n\treturn ifaces, nil\n}\n\nfunc fatalErr(err error) {\n\t// TODO: expose in UI.\n\tlog.Printf(\"fatal error: %v\", err)\n}\n\nfunc randHex(n int) string {\n\tb := make([]byte, n)\n\trand.Read(b)\n\treturn hex.EncodeToString(b)\n}\n"
  },
  {
    "path": "cmd/tailscale/multitun.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"os\"\n\n\t\"golang.zx2c4.com/wireguard/tun\"\n)\n\n// multiTUN implements a tun.Device that supports multiple\n// underlying devices. This is necessary because Android VPN devices\n// have static configurations and wgengine.NewUserspaceEngine\n// assumes a single static tun.Device.\ntype multiTUN struct {\n\t// devices is for adding new devices.\n\tdevices chan tun.Device\n\t// event is the combined event channel from all active devices.\n\tevents chan tun.Event\n\n\tclose    chan struct{}\n\tcloseErr chan error\n\n\treads        chan ioRequest\n\twrites       chan ioRequest\n\tflushes      chan chan error\n\tmtus         chan chan mtuReply\n\tnames        chan chan nameReply\n\tshutdowns    chan struct{}\n\tshutdownDone chan struct{}\n}\n\n// tunDevice wraps and drives a single run.Device.\ntype tunDevice struct {\n\tdev tun.Device\n\t// close closes the device.\n\tclose     chan struct{}\n\tcloseDone chan error\n\t// readDone is notified when the read goroutine is done.\n\treadDone chan struct{}\n}\n\ntype ioRequest struct {\n\tdata   []byte\n\toffset int\n\treply  chan<- ioReply\n}\n\ntype ioReply struct {\n\tbytes int\n\terr   error\n}\n\ntype mtuReply struct {\n\tmtu int\n\terr error\n}\n\ntype nameReply struct {\n\tname string\n\terr  error\n}\n\nfunc newTUNDevices() *multiTUN {\n\td := &multiTUN{\n\t\tdevices:      make(chan tun.Device),\n\t\tevents:       make(chan tun.Event),\n\t\tclose:        make(chan struct{}),\n\t\tcloseErr:     make(chan error),\n\t\treads:        make(chan ioRequest),\n\t\twrites:       make(chan ioRequest),\n\t\tflushes:      make(chan chan error),\n\t\tmtus:         make(chan chan mtuReply),\n\t\tnames:        make(chan chan nameReply),\n\t\tshutdowns:    make(chan struct{}),\n\t\tshutdownDone: make(chan struct{}),\n\t}\n\tgo d.run()\n\treturn d\n}\n\nfunc (d *multiTUN) run() {\n\tvar devices []*tunDevice\n\t// readDone is the readDone channel of the device being read from.\n\tvar readDone chan struct{}\n\t// runDone is the closeDone channel of the device being written to.\n\tvar runDone chan error\n\tfor {\n\t\tselect {\n\t\tcase <-readDone:\n\t\t\t// The oldest device has reached EOF, replace it.\n\t\t\tn := copy(devices, devices[1:])\n\t\t\tdevices = devices[:n]\n\t\t\tif len(devices) > 0 {\n\t\t\t\t// Start reading from the next device.\n\t\t\t\tdev := devices[0]\n\t\t\t\treadDone = dev.readDone\n\t\t\t\tgo d.readFrom(dev)\n\t\t\t}\n\t\tcase <-runDone:\n\t\t\t// A device completed runDevice, replace it.\n\t\t\tif len(devices) > 0 {\n\t\t\t\tdev := devices[len(devices)-1]\n\t\t\t\trunDone = dev.closeDone\n\t\t\t\tgo d.runDevice(dev)\n\t\t\t}\n\t\tcase <-d.shutdowns:\n\t\t\t// Shut down all devices.\n\t\t\tfor _, dev := range devices {\n\t\t\t\tclose(dev.close)\n\t\t\t\t<-dev.closeDone\n\t\t\t\t<-dev.readDone\n\t\t\t}\n\t\t\tdevices = nil\n\t\t\td.shutdownDone <- struct{}{}\n\t\tcase <-d.close:\n\t\t\tvar derr error\n\t\t\tfor _, dev := range devices {\n\t\t\t\tif err := <-dev.closeDone; err != nil {\n\t\t\t\t\tderr = err\n\t\t\t\t}\n\t\t\t}\n\t\t\td.closeErr <- derr\n\t\t\treturn\n\t\tcase dev := <-d.devices:\n\t\t\tif len(devices) > 0 {\n\t\t\t\t// Ask the most recent device to stop.\n\t\t\t\tprev := devices[len(devices)-1]\n\t\t\t\tclose(prev.close)\n\t\t\t}\n\t\t\twrap := &tunDevice{\n\t\t\t\tdev:       dev,\n\t\t\t\tclose:     make(chan struct{}),\n\t\t\t\tcloseDone: make(chan error),\n\t\t\t\treadDone:  make(chan struct{}, 1),\n\t\t\t}\n\t\t\tif len(devices) == 0 {\n\t\t\t\t// Start using this first device.\n\t\t\t\treadDone = wrap.readDone\n\t\t\t\tgo d.readFrom(wrap)\n\t\t\t\trunDone = wrap.closeDone\n\t\t\t\tgo d.runDevice(wrap)\n\t\t\t}\n\t\t\tdevices = append(devices, wrap)\n\t\tcase f := <-d.flushes:\n\t\t\tvar err error\n\t\t\tif len(devices) > 0 {\n\t\t\t\tdev := devices[len(devices)-1]\n\t\t\t\terr = dev.dev.Flush()\n\t\t\t}\n\t\t\tf <- err\n\t\tcase m := <-d.mtus:\n\t\t\tr := mtuReply{mtu: defaultMTU}\n\t\t\tif len(devices) > 0 {\n\t\t\t\tdev := devices[len(devices)-1]\n\t\t\t\tr.mtu, r.err = dev.dev.MTU()\n\t\t\t}\n\t\t\tm <- r\n\t\tcase n := <-d.names:\n\t\t\tvar r nameReply\n\t\t\tif len(devices) > 0 {\n\t\t\t\tdev := devices[len(devices)-1]\n\t\t\t\tr.name, r.err = dev.dev.Name()\n\t\t\t}\n\t\t\tn <- r\n\t\t}\n\t}\n}\n\nfunc (d *multiTUN) readFrom(dev *tunDevice) {\n\tdefer func() {\n\t\tdev.readDone <- struct{}{}\n\t}()\n\tfor {\n\t\tselect {\n\t\tcase r := <-d.reads:\n\t\t\tn, err := dev.dev.Read(r.data, r.offset)\n\t\t\tstop := false\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase <-dev.close:\n\t\t\t\t\tstop = true\n\t\t\t\t\terr = nil\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t\tr.reply <- ioReply{n, err}\n\t\t\tif stop {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-d.close:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (d *multiTUN) runDevice(dev *tunDevice) {\n\tdefer func() {\n\t\t// The documentation for https://developer.android.com/reference/android/net/VpnService.Builder#establish()\n\t\t// states that \"Therefore, after draining the old file\n\t\t// descriptor...\", but pending Reads are never unblocked\n\t\t// when a new descriptor is created.\n\t\t//\n\t\t// Close it instead and hope that no packets are lost.\n\t\tdev.closeDone <- dev.dev.Close()\n\t}()\n\t// Pump device events.\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase e := <-dev.dev.Events():\n\t\t\t\td.events <- e\n\t\t\tcase <-dev.close:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\tfor {\n\t\tselect {\n\t\tcase w := <-d.writes:\n\t\t\tn, err := dev.dev.Write(w.data, w.offset)\n\t\t\tw.reply <- ioReply{n, err}\n\t\tcase <-dev.close:\n\t\t\t// Device closed.\n\t\t\treturn\n\t\tcase <-d.close:\n\t\t\t// Multi-device closed.\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (d *multiTUN) add(dev tun.Device) {\n\td.devices <- dev\n}\n\nfunc (d *multiTUN) File() *os.File {\n\t// The underlying file descriptor is not constant on Android.\n\t// Let's hope no-one uses it.\n\tpanic(\"not available on Android\")\n}\n\nfunc (d *multiTUN) Read(data []byte, offset int) (int, error) {\n\tr := make(chan ioReply)\n\td.reads <- ioRequest{data, offset, r}\n\trep := <-r\n\treturn rep.bytes, rep.err\n}\n\nfunc (d *multiTUN) Write(data []byte, offset int) (int, error) {\n\tr := make(chan ioReply)\n\td.writes <- ioRequest{data, offset, r}\n\trep := <-r\n\treturn rep.bytes, rep.err\n}\n\nfunc (d *multiTUN) Flush() error {\n\tr := make(chan error)\n\td.flushes <- r\n\treturn <-r\n}\n\nfunc (d *multiTUN) MTU() (int, error) {\n\tr := make(chan mtuReply)\n\td.mtus <- r\n\trep := <-r\n\treturn rep.mtu, rep.err\n}\n\nfunc (d *multiTUN) Name() (string, error) {\n\tr := make(chan nameReply)\n\td.names <- r\n\trep := <-r\n\treturn rep.name, rep.err\n}\n\nfunc (d *multiTUN) Events() chan tun.Event {\n\treturn d.events\n}\n\nfunc (d *multiTUN) Shutdown() {\n\td.shutdowns <- struct{}{}\n\t<-d.shutdownDone\n}\n\nfunc (d *multiTUN) Close() error {\n\tclose(d.close)\n\treturn <-d.closeErr\n}\n"
  },
  {
    "path": "cmd/tailscale/pprof.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build pprof\n// +build pprof\n\npackage main\n\nimport (\n\t\"net/http\"\n\t_ \"net/http/pprof\"\n)\n\nfunc init() {\n\tgo func() {\n\t\thttp.ListenAndServe(\":6060\", nil)\n\t}()\n}\n"
  },
  {
    "path": "cmd/tailscale/store.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"encoding/base64\"\n\n\t\"tailscale.com/ipn\"\n\n\t\"github.com/tailscale/tailscale-android/jni\"\n)\n\n// stateStore is the Go interface for a persistent storage\n// backend by androidx.security.crypto.EncryptedSharedPreferences (see\n// App.java).\ntype stateStore struct {\n\tjvm *jni.JVM\n\t// appCtx is the global Android app context.\n\tappCtx jni.Object\n\n\t// Cached method ids on appCtx.\n\tencrypt jni.MethodID\n\tdecrypt jni.MethodID\n}\n\nfunc newStateStore(jvm *jni.JVM, appCtx jni.Object) *stateStore {\n\ts := &stateStore{\n\t\tjvm:    jvm,\n\t\tappCtx: appCtx,\n\t}\n\tjni.Do(jvm, func(env *jni.Env) error {\n\t\tappCls := jni.GetObjectClass(env, appCtx)\n\t\ts.encrypt = jni.GetMethodID(\n\t\t\tenv, appCls,\n\t\t\t\"encryptToPref\", \"(Ljava/lang/String;Ljava/lang/String;)V\",\n\t\t)\n\t\ts.decrypt = jni.GetMethodID(\n\t\t\tenv, appCls,\n\t\t\t\"decryptFromPref\", \"(Ljava/lang/String;)Ljava/lang/String;\",\n\t\t)\n\t\treturn nil\n\t})\n\treturn s\n}\n\nfunc prefKeyFor(id ipn.StateKey) string {\n\treturn \"statestore-\" + string(id)\n}\n\nfunc (s *stateStore) ReadString(key string, def string) (string, error) {\n\tdata, err := s.read(key)\n\tif err != nil {\n\t\treturn def, err\n\t}\n\tif data == nil {\n\t\treturn def, nil\n\t}\n\treturn string(data), nil\n}\n\nfunc (s *stateStore) WriteString(key string, val string) error {\n\treturn s.write(key, []byte(val))\n}\n\nfunc (s *stateStore) ReadBool(key string, def bool) (bool, error) {\n\tdata, err := s.read(key)\n\tif err != nil {\n\t\treturn def, err\n\t}\n\tif data == nil {\n\t\treturn def, nil\n\t}\n\treturn string(data) == \"true\", nil\n}\n\nfunc (s *stateStore) WriteBool(key string, val bool) error {\n\tdata := []byte(\"false\")\n\tif val {\n\t\tdata = []byte(\"true\")\n\t}\n\treturn s.write(key, data)\n}\n\nfunc (s *stateStore) ReadState(id ipn.StateKey) ([]byte, error) {\n\tstate, err := s.read(prefKeyFor(id))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif state == nil {\n\t\treturn nil, ipn.ErrStateNotExist\n\t}\n\treturn state, nil\n}\n\nfunc (s *stateStore) WriteState(id ipn.StateKey, bs []byte) error {\n\tprefKey := prefKeyFor(id)\n\treturn s.write(prefKey, bs)\n}\n\nfunc (s *stateStore) read(key string) ([]byte, error) {\n\tvar data []byte\n\terr := jni.Do(s.jvm, func(env *jni.Env) error {\n\t\tjfile := jni.JavaString(env, key)\n\t\tplain, err := jni.CallObjectMethod(env, s.appCtx, s.decrypt,\n\t\t\tjni.Value(jfile))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tb64 := jni.GoString(env, jni.String(plain))\n\t\tif b64 == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tdata, err = base64.RawStdEncoding.DecodeString(b64)\n\t\treturn err\n\t})\n\treturn data, err\n}\n\nfunc (s *stateStore) write(key string, value []byte) error {\n\tbs64 := base64.RawStdEncoding.EncodeToString(value)\n\terr := jni.Do(s.jvm, func(env *jni.Env) error {\n\t\tjfile := jni.JavaString(env, key)\n\t\tjplain := jni.JavaString(env, bs64)\n\t\terr := jni.CallVoidMethod(env, s.appCtx, s.encrypt,\n\t\t\tjni.Value(jfile), jni.Value(jplain))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\treturn err\n}\n"
  },
  {
    "path": "cmd/tailscale/tools.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build tools\n// +build tools\n\npackage main\n\nimport (\n\t_ \"gioui.org/cmd/gogio\"\n)\n"
  },
  {
    "path": "cmd/tailscale/ui.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"time\"\n\n\t\"gioui.org/f32\"\n\t\"gioui.org/font/opentype\"\n\t\"gioui.org/io/pointer\"\n\t\"gioui.org/io/system\"\n\t\"gioui.org/layout\"\n\t\"gioui.org/op\"\n\t\"gioui.org/op/clip\"\n\t\"gioui.org/op/paint\"\n\t\"gioui.org/text\"\n\t\"gioui.org/unit\"\n\t\"gioui.org/widget\"\n\t\"gioui.org/widget/material\"\n\tqrcode \"github.com/skip2/go-qrcode\"\n\t\"golang.org/x/exp/shiny/materialdesign/icons\"\n\t\"inet.af/netaddr\"\n\t\"tailscale.com/client/tailscale/apitype\"\n\t\"tailscale.com/ipn\"\n\t\"tailscale.com/tailcfg\"\n\n\t_ \"embed\"\n\n\t\"eliasnaur.com/font/roboto/robotobold\"\n\t\"eliasnaur.com/font/roboto/robotoregular\"\n\n\t_ \"image/png\"\n)\n\ntype UI struct {\n\ttheme *material.Theme\n\tstore *stateStore\n\n\t// root is the scrollable list of the main UI.\n\troot layout.List\n\t// enabled is the switch for enabling or disabling the VPN.\n\tenabled widget.Bool\n\tsearch  widget.Editor\n\n\texitLAN widget.Bool\n\n\t// webSigin is the button for the web-based sign-in flow.\n\twebSignin widget.Clickable\n\n\t// googleSignin is the button for native Google Sign-in.\n\tgoogleSignin widget.Clickable\n\n\t// openExitDialog opens the exit node picker.\n\topenExitDialog widget.Clickable\n\n\tsigninType signinType\n\n\tsetLoginServer    bool\n\tloginServer       widget.Editor\n\tloginServerSave   widget.Clickable\n\tloginServerCancel widget.Clickable\n\n\tself  widget.Clickable\n\tpeers []widget.Clickable\n\n\t// exitDialog is state for the exit node dialog.\n\texitDialog struct {\n\t\tshow    bool\n\t\tdismiss Dismiss\n\t\texits   widget.Enum\n\t\tlist    layout.List\n\t}\n\n\trunningExit   bool // are we an exit node now?\n\n\tqr struct {\n\t\tshow bool\n\t\top   paint.ImageOp\n\t}\n\n\tintro struct {\n\t\tlist  layout.List\n\t\tstart widget.Clickable\n\t\tshow  bool\n\t}\n\n\tmenu struct {\n\t\topen    widget.Clickable\n\t\tdismiss Dismiss\n\t\tshow    bool\n\n\t\tuseLoginServer widget.Clickable\n\t\tcopy           widget.Clickable\n\t\treauth         widget.Clickable\n\t\tbug            widget.Clickable\n\t\tbeExit         widget.Clickable\n\t\texits          widget.Clickable\n\t\tlogout         widget.Clickable\n\t}\n\n\t// The current pop-up message, if any\n\tmessage struct {\n\t\ttext string\n\t\t// t0 is the time when the most recent message appeared.\n\t\tt0 time.Time\n\t}\n\n\tshareDialog struct {\n\t\tshow    bool\n\t\tdismiss Dismiss\n\t\tlist    layout.List\n\t\t// peers are the nodes ready to receive files.\n\t\ttargets []shareTarget\n\t\tloaded  bool\n\t\terror   error\n\t}\n\n\ticons struct {\n\t\tsearch     *widget.Icon\n\t\tmore       *widget.Icon\n\t\texitStatus *widget.Icon\n\t\tdone       *widget.Icon\n\t\terror      *widget.Icon\n\t\tlogo       paint.ImageOp\n\t\tgoogle     paint.ImageOp\n\t}\n}\n\ntype shareTarget struct {\n\tbtn     widget.Clickable\n\ttarget  *apitype.FileTarget\n\tinfo    FileSendInfo\n\tcancel  func()\n\tupdates <-chan FileSendInfo\n}\n\ntype signinType uint8\n\n// An UIPeer is either a peer or a section header\n// with the user information.\ntype UIPeer struct {\n\t// Owner of the peer.\n\tOwner tailcfg.UserID\n\t// Name is the owner's name in all caps (for section headers).\n\tName string\n\t// Peer is nil for section headers.\n\tPeer *tailcfg.Node\n}\n\n// menuItem describes an item in a popup menu.\ntype menuItem struct {\n\ttitle string\n\tbtn   *widget.Clickable\n}\n\nconst (\n\theaderColor = 0x496495\n\tinfoColor   = 0x3a517b\n\twhite       = 0xffffff\n)\n\nconst (\n\tkeyShowIntro = \"ui.showintro\"\n)\n\nconst (\n\tnoSignin signinType = iota\n\twebSignin\n\tgoogleSignin\n)\n\ntype (\n\tC = layout.Context\n\tD = layout.Dimensions\n)\n\nvar (\n\t//go:embed tailscale.png\n\ttailscaleLogo []byte\n\t//go:embed google.png\n\tgoogleLogo []byte\n)\n\nfunc newUI(store *stateStore) (*UI, error) {\n\tsearchIcon, err := widget.NewIcon(icons.ActionSearch)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmoreIcon, err := widget.NewIcon(icons.NavigationMoreVert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\texitStatus, err := widget.NewIcon(icons.NavigationMenu)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdoneIcon, err := widget.NewIcon(icons.ActionCheckCircle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terrorIcon, err := widget.NewIcon(icons.AlertErrorOutline)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlogo, _, err := image.Decode(bytes.NewReader(tailscaleLogo))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgoogle, _, err := image.Decode(bytes.NewReader(googleLogo))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tface, err := opentype.Parse(robotoregular.TTF)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to parse font: %v\", err))\n\t}\n\tfaceBold, err := opentype.Parse(robotobold.TTF)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to parse font: %v\", err))\n\t}\n\tfonts := []text.FontFace{\n\t\t{Font: text.Font{Typeface: \"Roboto\"}, Face: face},\n\t\t{Font: text.Font{Typeface: \"Roboto\", Weight: text.Bold}, Face: faceBold},\n\t}\n\tui := &UI{\n\t\ttheme: material.NewTheme(fonts),\n\t\tstore: store,\n\t}\n\tui.intro.show, _ = store.ReadBool(keyShowIntro, true)\n\tui.icons.search = searchIcon\n\tui.icons.more = moreIcon\n\tui.icons.exitStatus = exitStatus\n\tui.icons.done = doneIcon\n\tui.icons.error = errorIcon\n\tui.icons.logo = paint.NewImageOp(logo)\n\tui.icons.google = paint.NewImageOp(google)\n\tui.root.Axis = layout.Vertical\n\tui.intro.list.Axis = layout.Vertical\n\tui.search.SingleLine = true\n\tui.loginServer.SingleLine = true\n\tui.exitDialog.list.Axis = layout.Vertical\n\tui.shareDialog.list.Axis = layout.Vertical\n\treturn ui, nil\n}\n\nfunc mulAlpha(c color.NRGBA, alpha uint8) color.NRGBA {\n\tc.A = uint8(uint32(c.A) * uint32(alpha) / 0xff)\n\treturn c\n}\n\nfunc (ui *UI) onBack() bool {\n\tb := ui.activeDialog()\n\tif b == nil {\n\t\treturn false\n\t}\n\t*b = false\n\treturn true\n}\n\nfunc (ui *UI) activeDialog() *bool {\n\tswitch {\n\tcase ui.qr.show:\n\t\treturn &ui.qr.show\n\tcase ui.menu.show:\n\t\treturn &ui.menu.show\n\tcase ui.shareDialog.show:\n\t\treturn &ui.shareDialog.show\n\tcase ui.exitDialog.show:\n\t\treturn &ui.exitDialog.show\n\t}\n\treturn nil\n}\n\nfunc (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientState) []UIEvent {\n\t// \"Get started\".\n\tif ui.intro.show {\n\t\tif ui.intro.start.Clicked() {\n\t\t\tui.store.WriteBool(keyShowIntro, false)\n\t\t\tui.intro.show = false\n\t\t}\n\t\tui.layoutIntro(gtx, sysIns)\n\t\treturn nil\n\t}\n\n\tvar events []UIEvent\n\n\tif ui.enabled.Changed() {\n\t\tevents = append(events, ConnectEvent{Enable: ui.enabled.Value})\n\t}\n\n\tfor _, e := range ui.search.Events() {\n\t\tif _, ok := e.(widget.ChangeEvent); ok {\n\t\t\tevents = append(events, SearchEvent{Query: ui.search.Text()})\n\t\t\tbreak\n\t\t}\n\t}\n\tfor ui.menu.open.Clicked() {\n\t\tui.menu.show = !ui.menu.show\n\t}\n\n\tnetmap := state.backend.NetworkMap\n\tvar (\n\t\tlocalName, localAddr string\n\t\texpiry               time.Time\n\t\tuserID               tailcfg.UserID\n\t\texitID               tailcfg.StableNodeID\n\t)\n\tif netmap != nil {\n\t\tuserID = netmap.User\n\t\texpiry = netmap.Expiry\n\t\tlocalName = netmap.SelfNode.DisplayName(false)\n\t\tif addrs := netmap.Addresses; len(addrs) > 0 {\n\t\t\tlocalAddr = addrs[0].IP().String()\n\t\t}\n\t}\n\tif p := state.backend.Prefs; p != nil {\n\t\texitID = p.ExitNodeID\n\t}\n\tif d := &ui.exitDialog; d.show {\n\t\tif newID := tailcfg.StableNodeID(d.exits.Value); newID != exitID {\n\t\t\td.show = false\n\t\t\tevents = append(events, RouteAllEvent{newID})\n\t\t}\n\t} else {\n\t\td.exits.Value = string(exitID)\n\t}\n\tif ui.exitLAN.Changed() {\n\t\tevents = append(events, ExitAllowLANEvent(ui.exitLAN.Value))\n\t}\n\n\tif ui.googleSignin.Clicked() {\n\t\tui.signinType = googleSignin\n\t\tevents = append(events, GoogleAuthEvent{})\n\t}\n\n\tif ui.webSignin.Clicked() {\n\t\tui.signinType = webSignin\n\t\tevents = append(events, WebAuthEvent{})\n\t}\n\n\tif ui.loginServerSave.Clicked() {\n\t\ttext := ui.loginServer.Text()\n\t\tui.showMessage(gtx, \"Login server saved, relaunch the app\")\n\t\tevents = append(events, SetLoginServerEvent{URL: text})\n\t}\n\tif ui.loginServerCancel.Clicked() {\n\t\tui.setLoginServer = false\n\t}\n\n\tif ui.menuClicked(&ui.menu.useLoginServer) {\n\t\tui.setLoginServer = true\n\t\tsavedLoginServer, _ := ui.store.ReadString(customLoginServerPrefKey, \"\")\n\t\tui.loginServer.SetText(savedLoginServer)\n\t}\n\n\tif ui.menuClicked(&ui.menu.copy) && localAddr != \"\" {\n\t\tevents = append(events, CopyEvent{Text: localAddr})\n\t\tui.showCopied(gtx, localAddr)\n\t}\n\n\tif ui.menuClicked(&ui.menu.reauth) {\n\t\tevents = append(events, ReauthEvent{})\n\t}\n\n\tif ui.menuClicked(&ui.menu.bug) {\n\t\tevents = append(events, BugEvent{})\n\t\tui.showCopied(gtx, \"bug report marker to clipboard\")\n\t}\n\n\tif ui.menuClicked(&ui.menu.beExit) {\n\t\tui.runningExit = !ui.runningExit\n\t\tevents = append(events, BeExitNodeEvent(ui.runningExit))\n\t\tif ui.runningExit {\n\t\t\tui.showMessage(gtx, \"Running exit node\")\n\t\t} else {\n\t\t\tui.showMessage(gtx, \"Stopped running exit node\")\n\t\t}\n\t}\n\n\tif ui.menuClicked(&ui.menu.exits) || ui.openExitDialog.Clicked() {\n\t\tui.exitDialog.show = true\n\t}\n\n\tif ui.menuClicked(&ui.menu.logout) {\n\t\tevents = append(events, LogoutEvent{})\n\t}\n\n\tfor i := range ui.shareDialog.targets {\n\t\tt := &ui.shareDialog.targets[i]\n\t\tselect {\n\t\tcase t.info = <-t.updates:\n\t\tdefault:\n\t\t}\n\t\tif !t.btn.Clicked() {\n\t\t\tcontinue\n\t\t}\n\t\tswitch t.info.State {\n\t\tcase FileSendTransferring, FileSendConnecting:\n\t\t\tt.cancel()\n\t\t\tt.info.State = FileSendNotStarted\n\t\t\tt.updates = nil\n\t\t\tcontinue\n\t\t}\n\t\tt.info = FileSendInfo{\n\t\t\tState: FileSendConnecting,\n\t\t}\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tt.cancel = cancel\n\t\tupdates := make(chan FileSendInfo, 1)\n\t\tt.updates = updates\n\t\tevents = append(events, FileSendEvent{\n\t\t\tTarget:  t.target,\n\t\t\tContext: ctx,\n\t\t\tUpdates: func(info FileSendInfo) {\n\t\t\t\tselect {\n\t\t\t\tcase <-updates:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tupdates <- info\n\t\t\t},\n\t\t})\n\t}\n\n\tfor len(ui.peers) < len(state.Peers) {\n\t\tui.peers = append(ui.peers, widget.Clickable{})\n\t}\n\tif max := len(state.Peers); len(ui.peers) > max {\n\t\tui.peers = ui.peers[:max]\n\t}\n\n\tconst numHeaders = 6\n\tn := numHeaders + len(state.Peers)\n\tneedsLogin := state.backend.State == ipn.NeedsLogin\n\tif !needsLogin {\n\t\tui.qr.show = false\n\t}\n\trootGtx := gtx\n\tif ui.activeDialog() != nil {\n\t\trootGtx.Queue = nil\n\t}\n\tui.root.Layout(rootGtx, n, func(gtx C, idx int) D {\n\t\tvar in layout.Inset\n\t\tif idx == n-1 {\n\t\t\t// The last list element includes the bottom system\n\t\t\t// inset.\n\t\t\tin.Bottom = sysIns.Bottom\n\t\t}\n\t\treturn in.Layout(gtx, func(gtx C) D {\n\t\t\tswitch idx {\n\t\t\tcase 0:\n\t\t\t\treturn ui.layoutTop(gtx, sysIns, &state.backend)\n\t\t\tcase 1:\n\t\t\t\tif netmap == nil || state.backend.State < ipn.Stopped {\n\t\t\t\t\treturn D{}\n\t\t\t\t}\n\t\t\t\tfor ui.self.Clicked() {\n\t\t\t\t\tevents = append(events, CopyEvent{Text: localAddr})\n\t\t\t\t\tui.showCopied(gtx, localAddr)\n\t\t\t\t}\n\t\t\t\treturn ui.layoutLocal(gtx, sysIns, localName, localAddr)\n\t\t\tcase 2:\n\t\t\t\treturn ui.layoutExitStatus(gtx, &state.backend)\n\t\t\tcase 3:\n\t\t\t\tif state.backend.State < ipn.Stopped {\n\t\t\t\t\treturn D{}\n\t\t\t\t}\n\t\t\t\treturn ui.layoutSearchbar(gtx, sysIns)\n\t\t\tcase 4:\n\t\t\t\tif !needsLogin || state.backend.LostInternet {\n\t\t\t\t\treturn D{}\n\t\t\t\t}\n\t\t\t\treturn ui.layoutSignIn(gtx, &state.backend)\n\t\t\tcase 5:\n\t\t\t\tif !state.backend.LostInternet {\n\t\t\t\t\treturn D{}\n\t\t\t\t}\n\t\t\t\treturn ui.layoutDisconnected(gtx)\n\t\t\tdefault:\n\t\t\t\tif needsLogin {\n\t\t\t\t\treturn D{}\n\t\t\t\t}\n\t\t\t\tpidx := idx - numHeaders\n\t\t\t\tp := &state.Peers[pidx]\n\t\t\t\tif p.Peer == nil {\n\t\t\t\t\tname := p.Name\n\t\t\t\t\tif p.Owner == userID {\n\t\t\t\t\t\tname = \"MY DEVICES\"\n\t\t\t\t\t}\n\t\t\t\t\treturn ui.layoutSection(gtx, sysIns, name)\n\t\t\t\t} else {\n\t\t\t\t\tclk := &ui.peers[pidx]\n\t\t\t\t\tif clk.Clicked() {\n\t\t\t\t\t\tif addrs := p.Peer.Addresses; len(addrs) > 0 {\n\t\t\t\t\t\t\ta := addrs[0].IP().String()\n\t\t\t\t\t\t\tevents = append(events, CopyEvent{Text: a})\n\t\t\t\t\t\t\tui.showCopied(gtx, a)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn ui.layoutPeer(gtx, sysIns, p, userID, clk)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n\n\tui.layoutExitNodeDialog(gtx, sysIns, state.backend.Exits)\n\n\tui.layoutShareDialog(gtx, sysIns)\n\n\t// Popup messages.\n\tui.layoutMessage(gtx, sysIns)\n\n\t// 3-dots menu.\n\tif ui.menu.show {\n\t\tui.layoutMenu(gtx, sysIns, expiry, exitID != \"\" || len(state.backend.Exits) > 0, needsLogin)\n\t}\n\n\tif ui.qr.show {\n\t\tui.layoutQR(gtx, sysIns)\n\t}\n\n\treturn events\n}\n\nfunc (ui *UI) layoutQR(gtx layout.Context, sysIns system.Insets) layout.Dimensions {\n\tfill{rgb(0x232323)}.Layout(gtx, gtx.Constraints.Max)\n\treturn layout.Center.Layout(gtx, func(gtx C) D {\n\t\treturn drawImage(gtx, ui.qr.op, unit.Dp(300))\n\t})\n}\n\nfunc (ui *UI) FillShareDialog(targets []*apitype.FileTarget, err error) {\n\tui.shareDialog.error = err\n\tui.shareDialog.loaded = true\n\ttargetSet := make(map[tailcfg.NodeID]int)\n\tif ui.shareDialog.show {\n\t\t// Update rather than replace list.\n\t\tfor i, t := range ui.shareDialog.targets {\n\t\t\ttargetSet[t.target.Node.ID] = i\n\t\t}\n\t} else {\n\t\tui.shareDialog.targets = nil\n\t}\n\tfor _, t := range targets {\n\t\tif i, ok := targetSet[t.Node.ID]; ok {\n\t\t\tui.shareDialog.targets[i].target = t\n\t\t} else {\n\t\t\tui.shareDialog.targets = append(ui.shareDialog.targets, shareTarget{target: t})\n\t\t}\n\t}\n}\n\nfunc (ui *UI) ShowShareDialog() {\n\tui.shareDialog.show = true\n}\n\nfunc (ui *UI) ShowMessage(msg string) {\n\tui.message.text = msg\n\tui.message.t0 = time.Now()\n}\n\nfunc (ui *UI) ShowQRCode(url string) {\n\tui.qr.show = true\n\tq, err := qrcode.New(url, qrcode.Medium)\n\tif err != nil {\n\t\tfatalErr(err)\n\t\treturn\n\t}\n\tui.qr.op = paint.NewImageOp(q.Image(512))\n}\n\n// Dismiss is a widget that detects pointer presses.\ntype Dismiss struct {\n}\n\nfunc (d *Dismiss) Add(gtx layout.Context, color color.NRGBA) {\n\tdefer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()\n\tpointer.InputOp{Tag: d, Types: pointer.Press}.Add(gtx.Ops)\n\tpaint.Fill(gtx.Ops, color)\n}\n\nfunc (d *Dismiss) Dismissed(gtx layout.Context) bool {\n\tfor _, e := range gtx.Events(d) {\n\t\tif e, ok := e.(pointer.Event); ok {\n\t\t\tif e.Type == pointer.Press {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (ui *UI) layoutExitStatus(gtx layout.Context, state *BackendState) layout.Dimensions {\n\tvar bg color.NRGBA\n\tvar text string\n\tswitch state.ExitStatus {\n\tcase ExitNone:\n\t\treturn D{}\n\tcase ExitOffline:\n\t\ttext = \"Exit node offline\"\n\t\tbg = rgb(0xc65835)\n\tcase ExitOnline:\n\t\ttext = \"Using exit node\"\n\t\tbg = rgb(0x338b51)\n\t}\n\tpaint.Fill(gtx.Ops, bg)\n\treturn material.Clickable(gtx, &ui.openExitDialog, func(gtx C) D {\n\t\tgtx.Constraints.Min.X = gtx.Constraints.Max.X\n\t\treturn layout.Inset{\n\t\t\tTop:    unit.Dp(12),\n\t\t\tBottom: unit.Dp(12),\n\t\t\tRight:  unit.Dp(24),\n\t\t\tLeft:   unit.Dp(24),\n\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\treturn layout.Flex{Alignment: layout.Middle}.Layout(gtx,\n\t\t\t\tlayout.Flexed(1, func(gtx C) D {\n\t\t\t\t\treturn layout.Flex{Axis: layout.Vertical}.Layout(gtx,\n\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\tlbl := material.Body2(ui.theme, text)\n\t\t\t\t\t\t\tlbl.Color = rgb(white)\n\t\t\t\t\t\t\treturn lbl.Layout(gtx)\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\tnode := material.Body2(ui.theme, state.Exit.Label)\n\t\t\t\t\t\t\tnode.Color = argb(0x88ffffff)\n\t\t\t\t\t\t\treturn node.Layout(gtx)\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t}),\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\treturn ui.icons.exitStatus.Layout(gtx, rgb(white))\n\t\t\t\t}),\n\t\t\t)\n\t\t})\n\t})\n}\n\n// layoutSignIn lays out the sign in button(s).\nfunc (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimensions {\n\treturn layout.Inset{Top: unit.Dp(48), Left: unit.Dp(48), Right: unit.Dp(48)}.Layout(gtx, func(gtx C) D {\n\t\tconst (\n\t\t\ttextColor = 0x555555\n\t\t)\n\n\t\tborder := widget.Border{Color: rgb(textColor), CornerRadius: unit.Dp(4), Width: unit.Px(1)}\n\n\t\tif ui.setLoginServer {\n\t\t\treturn layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\treturn layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\treturn Background{Color: rgb(0xe3e2ea), CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\treturn layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\treturn layout.Flex{Alignment: layout.Middle}.Layout(gtx,\n\t\t\t\t\t\t\t\t\tlayout.Flexed(1,\n\t\t\t\t\t\t\t\t\t\tmaterial.Editor(ui.theme, &ui.loginServer, \"https://controlplane.tailscale.com\").Layout,\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\treturn layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\treturn border.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\tbutton := material.Button(ui.theme, &ui.loginServerSave, \"Save and restart\")\n\t\t\t\t\t\t\tbutton.Background = color.NRGBA{} // transparent\n\t\t\t\t\t\t\tbutton.Color = rgb(textColor)\n\t\t\t\t\t\t\treturn button.Layout(gtx)\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\treturn border.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\tbutton := material.Button(ui.theme, &ui.loginServerCancel, \"Cancel\")\n\t\t\t\t\t\tbutton.Background = color.NRGBA{} // transparent\n\t\t\t\t\t\tbutton.Color = rgb(textColor)\n\t\t\t\t\t\treturn button.Layout(gtx)\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\n\t\treturn layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\tif !googleSignInEnabled() {\n\t\t\t\t\treturn D{}\n\t\t\t\t}\n\t\t\t\treturn layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\tsignin := material.ButtonLayout(ui.theme, &ui.googleSignin)\n\t\t\t\t\tsignin.Background = color.NRGBA{} // transparent\n\n\t\t\t\t\treturn ui.withLoader(gtx, ui.signinType == googleSignin, func(gtx C) D {\n\t\t\t\t\t\treturn border.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\tif ui.signinType != noSignin {\n\t\t\t\t\t\t\t\tgtx.Queue = nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn signin.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\tgtx.Constraints.Max.Y = gtx.Px(unit.Dp(48))\n\t\t\t\t\t\t\t\treturn layout.Flex{Alignment: layout.Middle}.Layout(gtx,\n\t\t\t\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\t\t\t\treturn layout.Inset{Right: unit.Dp(4)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\t\t\t\treturn drawImage(gtx, ui.icons.google, unit.Dp(16))\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\t\t\t\treturn layout.Inset{Top: unit.Dp(10), Bottom: unit.Dp(10)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\t\t\t\tl := material.Body2(ui.theme, \"Sign in with Google\")\n\t\t\t\t\t\t\t\t\t\t\tl.Color = rgb(textColor)\n\t\t\t\t\t\t\t\t\t\t\treturn l.Layout(gtx)\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}),\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\tlabel := \"Sign in with other\"\n\t\t\t\tif !googleSignInEnabled() {\n\t\t\t\t\tlabel = \"Sign in\"\n\t\t\t\t}\n\t\t\t\treturn ui.withLoader(gtx, ui.signinType == webSignin, func(gtx C) D {\n\t\t\t\t\treturn border.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\tif ui.signinType != noSignin {\n\t\t\t\t\t\t\tgtx.Queue = nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsignin := material.Button(ui.theme, &ui.webSignin, label)\n\t\t\t\t\t\tsignin.Background = color.NRGBA{} // transparent\n\t\t\t\t\t\tsignin.Color = rgb(textColor)\n\t\t\t\t\t\treturn signin.Layout(gtx)\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}),\n\t\t)\n\t})\n}\n\nfunc (ui *UI) withLoader(gtx layout.Context, loading bool, w layout.Widget) layout.Dimensions {\n\tcons := gtx.Constraints\n\treturn layout.Stack{Alignment: layout.W}.Layout(gtx,\n\t\tlayout.Stacked(func(gtx C) D {\n\t\t\tgtx.Constraints = cons\n\t\t\treturn w(gtx)\n\t\t}),\n\t\tlayout.Stacked(func(gtx C) D {\n\t\t\tif !loading {\n\t\t\t\treturn D{}\n\t\t\t}\n\t\t\treturn layout.Inset{Left: unit.Dp(16)}.Layout(gtx, func(gtx C) D {\n\t\t\t\tgtx.Constraints.Min = image.Point{\n\t\t\t\t\tX: gtx.Px(unit.Dp(16)),\n\t\t\t\t}\n\t\t\t\treturn material.Loader(ui.theme).Layout(gtx)\n\t\t\t})\n\t\t}),\n\t)\n}\n\n// layoutDisconnected lays out the \"please connect to the internet\"\n// message.\nfunc (ui *UI) layoutDisconnected(gtx layout.Context) layout.Dimensions {\n\treturn layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D {\n\t\treturn layout.Flex{Axis: layout.Vertical}.Layout(gtx,\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\treturn layout.Inset{Top: unit.Dp(8)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\ttitle := material.H6(ui.theme, \"No internet connection\")\n\t\t\t\t\ttitle.Alignment = text.Middle\n\t\t\t\t\treturn title.Layout(gtx)\n\t\t\t\t})\n\t\t\t}),\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\treturn layout.Inset{Top: unit.Dp(8)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\tmsg := material.Body2(ui.theme, \"Tailscale is paused while your device is offline. Please reconnect to the internet.\")\n\t\t\t\t\tmsg.Alignment = text.Middle\n\t\t\t\t\treturn msg.Layout(gtx)\n\t\t\t\t})\n\t\t\t}),\n\t\t)\n\t})\n}\n\n// layoutIntro lays out the intro page with the logo and terms.\nfunc (ui *UI) layoutIntro(gtx layout.Context, sysIns system.Insets) {\n\tfill{rgb(0x232323)}.Layout(gtx, gtx.Constraints.Max)\n\tui.intro.list.Layout(gtx, 1, func(gtx C, idx int) D {\n\t\treturn layout.Flex{Axis: layout.Vertical}.Layout(gtx,\n\t\t\t// 9 dot logo.\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\treturn layout.Inset{Top: unit.Dp(80), Bottom: unit.Dp(48)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\treturn layout.N.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\tsz := gtx.Px(unit.Dp(72))\n\t\t\t\t\t\tdrawLogo(gtx.Ops, sz)\n\t\t\t\t\t\treturn layout.Dimensions{Size: image.Pt(sz, sz)}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}),\n\t\t\t// \"tailscale\".\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\treturn layout.N.Layout(gtx, func(gtx C) D {\n\t\t\t\t\treturn drawImage(gtx, ui.icons.logo, unit.Dp(200))\n\t\t\t\t})\n\t\t\t}),\n\t\t\t// Terms.\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\treturn layout.Inset{\n\t\t\t\t\tTop:   unit.Dp(48),\n\t\t\t\t\tLeft:  unit.Dp(32),\n\t\t\t\t\tRight: unit.Dp(32),\n\t\t\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\tterms := material.Body2(ui.theme, termsText)\n\t\t\t\t\tterms.Color = rgb(0xbfbfbf)\n\t\t\t\t\tterms.Alignment = text.Middle\n\t\t\t\t\treturn terms.Layout(gtx)\n\t\t\t\t})\n\t\t\t}),\n\t\t\t// \"Get started\".\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\treturn layout.Inset{\n\t\t\t\t\tTop:    unit.Dp(16),\n\t\t\t\t\tLeft:   unit.Dp(16),\n\t\t\t\t\tRight:  unit.Dp(16),\n\t\t\t\t\tBottom: unit.Add(gtx.Metric, sysIns.Bottom),\n\t\t\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\tstart := material.Button(ui.theme, &ui.intro.start, \"Get Started\")\n\t\t\t\t\tstart.Inset = layout.UniformInset(unit.Dp(16))\n\t\t\t\t\tstart.CornerRadius = unit.Dp(16)\n\t\t\t\t\tstart.Background = rgb(0x496495)\n\t\t\t\t\tstart.TextSize = unit.Sp(20)\n\t\t\t\t\treturn start.Layout(gtx)\n\t\t\t\t})\n\t\t\t}),\n\t\t)\n\t})\n}\n\n// menuClicked is like btn.Clicked, but also closes the menu if true.\nfunc (ui *UI) menuClicked(btn *widget.Clickable) bool {\n\tcl := btn.Clicked()\n\tif cl {\n\t\tui.menu.show = false\n\t}\n\treturn cl\n}\n\n// layoutShareDialog lays out the file sharing dialog shown on file send intents (ACTION_SEND, ACTION_SEND_MULTIPLE).\nfunc (ui *UI) layoutShareDialog(gtx layout.Context, sysIns system.Insets) {\n\td := &ui.shareDialog\n\tif d.dismiss.Dismissed(gtx) {\n\t\tui.shareDialog.show = false\n\t}\n\tif !d.show {\n\t\treturn\n\t}\n\td.dismiss.Add(gtx, argb(0x66000000))\n\tlayout.Inset{\n\t\tTop:    unit.Add(gtx.Metric, sysIns.Top, unit.Dp(16)),\n\t\tRight:  unit.Add(gtx.Metric, sysIns.Right, unit.Dp(16)),\n\t\tBottom: unit.Add(gtx.Metric, sysIns.Bottom, unit.Dp(16)),\n\t\tLeft:   unit.Add(gtx.Metric, sysIns.Left, unit.Dp(16)),\n\t}.Layout(gtx, func(gtx C) D {\n\t\treturn layout.Center.Layout(gtx, func(gtx C) D {\n\t\t\tgtx.Constraints.Min.X = gtx.Px(unit.Dp(250))\n\t\t\tgtx.Constraints.Max.X = gtx.Constraints.Min.X\n\t\t\treturn layoutDialog(gtx, func(gtx C) D {\n\t\t\t\treturn layout.Flex{Axis: layout.Vertical}.Layout(gtx,\n\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t// Header.\n\t\t\t\t\t\td := layout.Inset{\n\t\t\t\t\t\t\tTop:    unit.Dp(16),\n\t\t\t\t\t\t\tRight:  unit.Dp(20),\n\t\t\t\t\t\t\tLeft:   unit.Dp(20),\n\t\t\t\t\t\t\tBottom: unit.Dp(16),\n\t\t\t\t\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\tl := material.Body1(ui.theme, \"Share via Tailscale\")\n\t\t\t\t\t\t\tl.Font.Weight = text.Bold\n\t\t\t\t\t\t\treturn l.Layout(gtx)\n\t\t\t\t\t\t})\n\t\t\t\t\t\t// Swallow clicks to title.\n\t\t\t\t\t\tvar c widget.Clickable\n\t\t\t\t\t\tgtx.Queue = nil\n\t\t\t\t\t\treturn c.Layout(gtx, func(gtx C) D { return d })\n\t\t\t\t\t}),\n\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\tif d.loaded {\n\t\t\t\t\t\t\treturn D{}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn layout.UniformInset(unit.Dp(50)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\treturn layout.Center.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\tsz := gtx.Px(unit.Dp(32))\n\t\t\t\t\t\t\t\tgtx.Constraints.Min = image.Pt(sz, sz)\n\t\t\t\t\t\t\t\tgtx.Constraints.Max = gtx.Constraints.Min\n\t\t\t\t\t\t\t\treturn material.Loader(ui.theme).Layout(gtx)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\t}),\n\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\tif d.error == nil {\n\t\t\t\t\t\t\treturn D{}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsz := gtx.Px(unit.Dp(50))\n\t\t\t\t\t\tgtx.Constraints.Min.Y = sz\n\t\t\t\t\t\treturn layout.UniformInset(unit.Dp(20)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\treturn layout.W.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\treturn material.Body2(ui.theme, d.error.Error()).Layout(gtx)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\t}),\n\t\t\t\t\tlayout.Flexed(1, func(gtx C) D {\n\t\t\t\t\t\tgtx.Constraints.Min.Y = 0\n\t\t\t\t\t\treturn d.list.Layout(gtx, len(d.targets), func(gtx C, idx int) D {\n\t\t\t\t\t\t\tnode := &d.targets[idx]\n\t\t\t\t\t\t\ttarget := node.target.Node\n\t\t\t\t\t\t\tlbl := target.ComputedName\n\t\t\t\t\t\t\toffline := target.Online != nil && !*target.Online\n\t\t\t\t\t\t\tif offline {\n\t\t\t\t\t\t\t\tlbl = lbl + \" (offline)\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tw := material.Body2(ui.theme, lbl)\n\t\t\t\t\t\t\tif offline {\n\t\t\t\t\t\t\t\tw.Color = rgb(0xbbbbbb)\n\t\t\t\t\t\t\t\tgtx.Queue = nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn material.Clickable(gtx, &node.btn, func(gtx C) D {\n\t\t\t\t\t\t\t\treturn layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\t\treturn layout.Flex{Alignment: layout.Middle}.Layout(gtx,\n\t\t\t\t\t\t\t\t\t\tlayout.Flexed(1, w.Layout),\n\t\t\t\t\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\t\t\t\t\tsz := gtx.Px(unit.Dp(16))\n\t\t\t\t\t\t\t\t\t\t\tgtx.Constraints.Min = image.Pt(sz, sz)\n\t\t\t\t\t\t\t\t\t\t\tswitch node.info.State {\n\t\t\t\t\t\t\t\t\t\t\tcase FileSendConnecting:\n\t\t\t\t\t\t\t\t\t\t\t\treturn material.Loader(ui.theme).Layout(gtx)\n\t\t\t\t\t\t\t\t\t\t\tcase FileSendTransferring:\n\t\t\t\t\t\t\t\t\t\t\t\treturn material.ProgressCircle(ui.theme, float32(node.info.Progress)).Layout(gtx)\n\t\t\t\t\t\t\t\t\t\t\tcase FileSendFailed:\n\t\t\t\t\t\t\t\t\t\t\t\treturn ui.icons.error.Layout(gtx, rgb(0xcc6539))\n\t\t\t\t\t\t\t\t\t\t\tcase FileSendComplete:\n\t\t\t\t\t\t\t\t\t\t\t\treturn ui.icons.done.Layout(gtx, ui.theme.Palette.ContrastBg)\n\t\t\t\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\t\t\t\treturn D{}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t})\n\t\t})\n\t})\n}\n\n// layoutExitNodeDialog lays out the exit node selection dialog.\nfunc (ui *UI) layoutExitNodeDialog(gtx layout.Context, sysIns system.Insets, exits []Peer) {\n\td := &ui.exitDialog\n\tif d.dismiss.Dismissed(gtx) {\n\t\td.show = false\n\t}\n\tif !d.show {\n\t\treturn\n\t}\n\td.dismiss.Add(gtx, argb(0x66000000))\n\tlayout.Inset{\n\t\tTop:    unit.Add(gtx.Metric, sysIns.Top, unit.Dp(16)),\n\t\tRight:  unit.Add(gtx.Metric, sysIns.Right, unit.Dp(16)),\n\t\tBottom: unit.Add(gtx.Metric, sysIns.Bottom, unit.Dp(16)),\n\t\tLeft:   unit.Add(gtx.Metric, sysIns.Left, unit.Dp(16)),\n\t}.Layout(gtx, func(gtx C) D {\n\t\treturn layout.Center.Layout(gtx, func(gtx C) D {\n\t\t\tgtx.Constraints.Min.X = gtx.Px(unit.Dp(250))\n\t\t\tgtx.Constraints.Max.X = gtx.Constraints.Min.X\n\t\t\treturn layoutDialog(gtx, func(gtx C) D {\n\t\t\t\treturn layout.Flex{Axis: layout.Vertical}.Layout(gtx,\n\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t// Header.\n\t\t\t\t\t\treturn layout.Inset{\n\t\t\t\t\t\t\tTop:    unit.Dp(16),\n\t\t\t\t\t\t\tRight:  unit.Dp(20),\n\t\t\t\t\t\t\tLeft:   unit.Dp(20),\n\t\t\t\t\t\t\tBottom: unit.Dp(16),\n\t\t\t\t\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\tl := material.Body1(ui.theme, \"Use exit node...\")\n\t\t\t\t\t\t\tl.Font.Weight = text.Bold\n\t\t\t\t\t\t\treturn l.Layout(gtx)\n\t\t\t\t\t\t})\n\t\t\t\t\t}),\n\t\t\t\t\tlayout.Flexed(1, func(gtx C) D {\n\t\t\t\t\t\tgtx.Constraints.Min.Y = 0\n\t\t\t\t\t\t// Add \"none\" exit node, then \"Allow LAN\" checkbox, then the exit nodes.\n\t\t\t\t\t\tn := len(exits) + 2\n\t\t\t\t\t\treturn d.list.Layout(gtx, n, func(gtx C, idx int) D {\n\t\t\t\t\t\t\tif idx == 0 {\n\t\t\t\t\t\t\t\tbtn := material.CheckBox(ui.theme, &ui.exitLAN, \"Allow LAN access\")\n\t\t\t\t\t\t\t\treturn layout.Inset{\n\t\t\t\t\t\t\t\t\tRight:  unit.Dp(16),\n\t\t\t\t\t\t\t\t\tLeft:   unit.Dp(16),\n\t\t\t\t\t\t\t\t\tBottom: unit.Dp(16),\n\t\t\t\t\t\t\t\t}.Layout(gtx, btn.Layout)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnode := Peer{Label: \"None\", Online: true}\n\t\t\t\t\t\t\tif idx >= 2 {\n\t\t\t\t\t\t\t\tnode = exits[idx-2]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tlbl := node.Label\n\t\t\t\t\t\t\tif !node.Online {\n\t\t\t\t\t\t\t\tlbl = lbl + \" (offline)\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbtn := material.RadioButton(ui.theme, &d.exits, string(node.ID), lbl)\n\t\t\t\t\t\t\tif !node.Online {\n\t\t\t\t\t\t\t\tbtn.Color = rgb(0xbbbbbb)\n\t\t\t\t\t\t\t\tbtn.IconColor = btn.Color\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn layout.Inset{\n\t\t\t\t\t\t\t\tRight:  unit.Dp(16),\n\t\t\t\t\t\t\t\tLeft:   unit.Dp(16),\n\t\t\t\t\t\t\t\tBottom: unit.Dp(16),\n\t\t\t\t\t\t\t}.Layout(gtx, btn.Layout)\n\t\t\t\t\t\t})\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t})\n\t\t})\n\t})\n}\n\nfunc layoutMenu(th *material.Theme, gtx layout.Context, items []menuItem, header layout.Widget) layout.Dimensions {\n\treturn layoutDialog(gtx, func(gtx C) D {\n\t\t// Lay out menu items twice; once for\n\t\t// measuring the widest item, once for actual layout.\n\t\tvar maxWidth int\n\t\tvar minWidth int\n\t\tchildren := []layout.FlexChild{\n\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\treturn layout.Inset{\n\t\t\t\t\tTop:    unit.Dp(16),\n\t\t\t\t\tRight:  unit.Dp(16),\n\t\t\t\t\tLeft:   unit.Dp(16),\n\t\t\t\t\tBottom: unit.Dp(4),\n\t\t\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\tgtx.Constraints.Min.X = minWidth\n\t\t\t\t\tdims := header(gtx)\n\t\t\t\t\tif w := dims.Size.X; w > maxWidth {\n\t\t\t\t\t\tmaxWidth = w\n\t\t\t\t\t}\n\t\t\t\t\treturn dims\n\t\t\t\t})\n\t\t\t}),\n\t\t}\n\t\tfor i := 0; i < len(items); i++ {\n\t\t\tit := &items[i]\n\t\t\tchildren = append(children, layout.Rigid(func(gtx C) D {\n\t\t\t\treturn material.Clickable(gtx, it.btn, func(gtx C) D {\n\t\t\t\t\treturn layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\tgtx.Constraints.Min.X = minWidth\n\t\t\t\t\t\tdims := material.Body1(th, it.title).Layout(gtx)\n\t\t\t\t\t\tif w := dims.Size.X; w > maxWidth {\n\t\t\t\t\t\t\tmaxWidth = w\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn dims\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}))\n\t\t}\n\t\tf := layout.Flex{Axis: layout.Vertical}\n\t\t// First pass: record and discard operations\n\t\t// and determine widest item.\n\t\tm := op.Record(gtx.Ops)\n\t\tf.Layout(gtx, children...)\n\t\tm.Stop()\n\t\t// Second pass: layout items with equal width.\n\t\tminWidth = maxWidth\n\t\treturn f.Layout(gtx, children...)\n\t})\n}\n\nfunc layoutDialog(gtx layout.Context, w layout.Widget) layout.Dimensions {\n\treturn widget.Border{Color: argb(0x33000000), CornerRadius: unit.Dp(2), Width: unit.Px(1)}.Layout(gtx, func(gtx C) D {\n\t\treturn Background{Color: rgb(0xfafafa), CornerRadius: unit.Dp(2)}.Layout(gtx, w)\n\t})\n}\n\n// layoutMenu lays out the menu activated by the 3 dots button.\nfunc (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.Time, showExits bool, needsLogin bool) {\n\tui.menu.dismiss.Add(gtx, color.NRGBA{})\n\tif ui.menu.dismiss.Dismissed(gtx) {\n\t\tui.menu.show = false\n\t}\n\tlayout.Inset{\n\t\tTop:   unit.Add(gtx.Metric, sysIns.Top, unit.Dp(2)),\n\t\tRight: unit.Add(gtx.Metric, sysIns.Right, unit.Dp(2)),\n\t}.Layout(gtx, func(gtx C) D {\n\t\treturn layout.NE.Layout(gtx, func(gtx C) D {\n\t\t\tmenu := &ui.menu\n\t\t\tif ui.setLoginServer {\n\t\t\t\treturn D{}\n\t\t\t}\n\t\t\tif needsLogin {\n\t\t\t\titems := []menuItem{\n\t\t\t\t\t{title: \"Use login server\", btn: &menu.useLoginServer},\n\t\t\t\t}\n\t\t\t\treturn layoutMenu(ui.theme, gtx, items, func(gtx C) D {\n\t\t\t\t\tl := material.Caption(ui.theme, \"Advanced settings\")\n\t\t\t\t\treturn l.Layout(gtx)\n\t\t\t\t})\n\t\t\t}\n\t\t\titems := []menuItem{\n\t\t\t\t{title: \"Copy my IP address\", btn: &menu.copy},\n\t\t\t}\n\t\t\tif showExits {\n\t\t\t\titems = append(items, menuItem{title: \"Use exit node...\", btn: &menu.exits})\n\t\t\t}\n\t\t\titems = append(items,\n\t\t\t\tmenuItem{title: \"Bug report\", btn: &menu.bug},\n\t\t\t\tmenuItem{title: \"Reauthenticate\", btn: &menu.reauth},\n\t\t\t\tmenuItem{title: \"Log out\", btn: &menu.logout},\n\t\t\t)\n\n\t\t\tvar title string\n\t\t\tif ui.runningExit {\n\t\t\t\ttitle = \"Stop running exit node\"\n\t\t\t} else {\n\t\t\t\ttitle = \"Run exit node\"\n\t\t\t}\n\t\t\titems = append(items, menuItem{title: title, btn: &menu.beExit})\n\n\t\t\treturn layoutMenu(ui.theme, gtx, items, func(gtx C) D {\n\t\t\t\tvar expiryStr string\n\t\t\t\tconst fmtStr = time.Stamp\n\t\t\t\tswitch {\n\t\t\t\tcase expiry.IsZero():\n\t\t\t\t\texpiryStr = \"Expires: (never)\"\n\t\t\t\tcase time.Now().After(expiry):\n\t\t\t\t\texpiryStr = fmt.Sprintf(\"Expired: %s\", expiry.Format(fmtStr))\n\t\t\t\tdefault:\n\t\t\t\t\texpiryStr = fmt.Sprintf(\"Expires: %s\", expiry.Format(fmtStr))\n\t\t\t\t}\n\t\t\t\tl := material.Caption(ui.theme, expiryStr)\n\t\t\t\tl.Color = rgb(0x8f8f8f)\n\t\t\t\treturn l.Layout(gtx)\n\t\t\t})\n\t\t})\n\t})\n}\n\nfunc (ui *UI) layoutMessage(gtx layout.Context, sysIns system.Insets) layout.Dimensions {\n\ts := ui.message.text\n\tif s == \"\" {\n\t\treturn D{}\n\t}\n\tnow := gtx.Now\n\td := now.Sub(ui.message.t0)\n\trem := 4*time.Second - d\n\tif rem < 0 {\n\t\treturn D{}\n\t}\n\top.InvalidateOp{At: now.Add(rem)}.Add(gtx.Ops)\n\treturn layout.S.Layout(gtx, func(gtx C) D {\n\t\treturn layout.Inset{\n\t\t\tLeft:   unit.Add(gtx.Metric, sysIns.Left, unit.Dp(8)),\n\t\t\tRight:  unit.Add(gtx.Metric, sysIns.Right, unit.Dp(8)),\n\t\t\tBottom: unit.Add(gtx.Metric, sysIns.Bottom, unit.Dp(8)),\n\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\treturn Background{Color: rgb(0x323232), CornerRadius: unit.Dp(5)}.Layout(gtx, func(gtx C) D {\n\t\t\t\treturn layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\tl := material.Body2(ui.theme, s)\n\t\t\t\t\tl.Color = rgb(0xdddddd)\n\t\t\t\t\treturn l.Layout(gtx)\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t})\n}\n\nfunc (ui *UI) showMessage(gtx layout.Context, msg string) {\n\tui.message.text = msg\n\tui.message.t0 = gtx.Now\n\top.InvalidateOp{}.Add(gtx.Ops)\n}\n\n// layoutPeer lays out a peer name and IP address (e.g.\n// \"localhost\\n100.100.100.101\")\nfunc (ui *UI) layoutPeer(gtx layout.Context, sysIns system.Insets, p *UIPeer, user tailcfg.UserID, clk *widget.Clickable) layout.Dimensions {\n\treturn material.Clickable(gtx, clk, func(gtx C) D {\n\t\treturn layout.Inset{\n\t\t\tTop:    unit.Dp(8),\n\t\t\tRight:  unit.Max(gtx.Metric, sysIns.Right, unit.Dp(16)),\n\t\t\tLeft:   unit.Max(gtx.Metric, sysIns.Left, unit.Dp(16)),\n\t\t\tBottom: unit.Dp(8),\n\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\tgtx.Constraints.Min.X = gtx.Constraints.Max.X\n\t\t\treturn layout.Flex{Axis: layout.Vertical}.Layout(gtx,\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\treturn layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\tname := p.Peer.DisplayName(p.Peer.User == user)\n\t\t\t\t\t\treturn material.H6(ui.theme, name).Layout(gtx)\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\tvar bestIP netaddr.IP // IP to show; first IPv4, or first IPv6 if no IPv4\n\t\t\t\t\tfor _, addr := range p.Peer.Addresses {\n\t\t\t\t\t\tif ip := addr.IP(); bestIP.IsZero() || bestIP.Is6() && ip.Is4() {\n\t\t\t\t\t\t\tbestIP = ip\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tl := material.Body2(ui.theme, bestIP.String())\n\t\t\t\t\tl.Color = rgb(0x434343)\n\t\t\t\t\treturn l.Layout(gtx)\n\t\t\t\t}),\n\t\t\t)\n\t\t})\n\t})\n}\n\n// layoutSection lays out a section title (e.g. \"My devices\").\nfunc (ui *UI) layoutSection(gtx layout.Context, sysIns system.Insets, title string) layout.Dimensions {\n\treturn Background{Color: rgb(0xe1e0e9)}.Layout(gtx, func(gtx C) D {\n\t\treturn layout.Inset{\n\t\t\tTop:    unit.Dp(16),\n\t\t\tRight:  unit.Max(gtx.Metric, sysIns.Right, unit.Dp(16)),\n\t\t\tLeft:   unit.Max(gtx.Metric, sysIns.Left, unit.Dp(16)),\n\t\t\tBottom: unit.Dp(16),\n\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\tl := material.Body1(ui.theme, title)\n\t\t\tl.Color = rgb(0x6f797d)\n\t\t\treturn l.Layout(gtx)\n\t\t})\n\t})\n}\n\n// layoutTop lays out the top controls: toggle, status and menu dots.\nfunc (ui *UI) layoutTop(gtx layout.Context, sysIns system.Insets, state *BackendState) layout.Dimensions {\n\tin := layout.Inset{\n\t\tTop:    unit.Dp(16),\n\t\tBottom: unit.Dp(16),\n\t}\n\treturn Background{Color: rgb(headerColor)}.Layout(gtx, func(gtx C) D {\n\t\treturn layout.Inset{\n\t\t\tTop:   sysIns.Top,\n\t\t\tRight: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)),\n\t\t\tLeft:  unit.Max(gtx.Metric, sysIns.Left, unit.Dp(16)),\n\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\treturn layout.Flex{Alignment: layout.Middle}.Layout(gtx,\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\treturn in.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\tif state.State <= ipn.NeedsLogin {\n\t\t\t\t\t\t\treturn D{}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsw := material.Switch(ui.theme, &ui.enabled, \"Enable VPN\")\n\t\t\t\t\t\tsw.Color.Enabled = rgb(white)\n\t\t\t\t\t\tif state.State < ipn.Stopped {\n\t\t\t\t\t\t\tsw.Color.Enabled = rgb(0xbbbbbb)\n\t\t\t\t\t\t\tsw.Color.Disabled = rgb(0xbbbbbb)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn sw.Layout(gtx)\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t\tlayout.Flexed(1, func(gtx C) D {\n\t\t\t\t\treturn in.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\treturn layout.Inset{Left: unit.Dp(16)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\tlbl := material.Body1(ui.theme, statusString(state.State))\n\t\t\t\t\t\t\tlbl.Color = rgb(0xffffff)\n\t\t\t\t\t\t\treturn lbl.Layout(gtx)\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\tbtn := material.IconButton(ui.theme, &ui.menu.open, ui.icons.more, \"Open menu\")\n\t\t\t\t\tbtn.Color = rgb(white)\n\t\t\t\t\tbtn.Background = color.NRGBA{}\n\t\t\t\t\treturn btn.Layout(gtx)\n\t\t\t\t}),\n\t\t\t)\n\t\t})\n\t})\n}\n\nfunc statusString(state ipn.State) string {\n\tswitch state {\n\tcase ipn.Stopped:\n\t\treturn \"Stopped\"\n\tcase ipn.Starting:\n\t\treturn \"Starting...\"\n\tcase ipn.Running:\n\t\treturn \"Active\"\n\tcase ipn.NeedsMachineAuth:\n\t\treturn \"Awaiting Approval\"\n\tcase ipn.NeedsLogin:\n\t\treturn \"Tailscale\"\n\tdefault:\n\t\treturn \"Loading...\"\n\t}\n}\n\nfunc (ui *UI) showCopied(gtx layout.Context, addr string) {\n\tui.showMessage(gtx, fmt.Sprintf(\"Copied %s\", addr))\n}\n\n// layoutLocal lays out the information box about the local node's\n// name and IP address.\nfunc (ui *UI) layoutLocal(gtx layout.Context, sysIns system.Insets, host, addr string) layout.Dimensions {\n\treturn Background{Color: rgb(headerColor)}.Layout(gtx, func(gtx C) D {\n\t\treturn layout.Inset{\n\t\t\tRight:  unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)),\n\t\t\tLeft:   unit.Max(gtx.Metric, sysIns.Left, unit.Dp(8)),\n\t\t\tBottom: unit.Dp(8),\n\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\treturn Background{Color: rgb(infoColor), CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx C) D {\n\t\t\t\treturn material.Clickable(gtx, &ui.self, func(gtx C) D {\n\t\t\t\t\treturn layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\tgtx.Constraints.Min.X = gtx.Constraints.Max.X\n\t\t\t\t\t\treturn layout.Flex{Axis: layout.Vertical}.Layout(gtx,\n\t\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\t\treturn layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx C) D {\n\t\t\t\t\t\t\t\t\tname := material.H6(ui.theme, host)\n\t\t\t\t\t\t\t\t\tname.Color = rgb(0xffffff)\n\t\t\t\t\t\t\t\t\treturn name.Layout(gtx)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\t\tname := material.Body2(ui.theme, addr)\n\t\t\t\t\t\t\t\tname.Color = rgb(0xc5ccd9)\n\t\t\t\t\t\t\t\treturn name.Layout(gtx)\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t})\n}\n\nfunc (ui *UI) layoutSearchbar(gtx layout.Context, sysIns system.Insets) layout.Dimensions {\n\treturn Background{Color: rgb(0xf0eff6)}.Layout(gtx, func(gtx C) D {\n\t\treturn layout.Inset{\n\t\t\tTop:    unit.Dp(8),\n\t\t\tRight:  unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)),\n\t\t\tLeft:   unit.Max(gtx.Metric, sysIns.Left, unit.Dp(8)),\n\t\t\tBottom: unit.Dp(8),\n\t\t}.Layout(gtx, func(gtx C) D {\n\t\t\treturn Background{Color: rgb(0xe3e2ea), CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx C) D {\n\t\t\t\treturn layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx C) D {\n\t\t\t\t\treturn layout.Flex{Alignment: layout.Middle}.Layout(gtx,\n\t\t\t\t\t\tlayout.Rigid(func(gtx C) D {\n\t\t\t\t\t\t\tcol := mulAlpha(ui.theme.Palette.Fg, 0xbb)\n\t\t\t\t\t\t\treturn ui.icons.search.Layout(gtx, col)\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tlayout.Flexed(1,\n\t\t\t\t\t\t\tmaterial.Editor(ui.theme, &ui.search, \"Search by machine name...\").Layout,\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t})\n}\n\n// drawLogo draws the Tailscale logo using vector operations.\nfunc drawLogo(ops *op.Ops, size int) {\n\tscale := float32(size) / 680\n\tdiscDia := 170 * scale\n\toff := 172 * 1.5 * scale\n\ttx := op.Offset(f32.Pt(off, 0))\n\tty := op.Offset(f32.Pt(0, off))\n\n\tdefer op.Offset(f32.Point{}).Push(ops).Pop()\n\n\t// First row of discs.\n\trow := op.Offset(f32.Point{}).Push(ops)\n\tdrawDisc(ops, discDia, rgb(0x54514d))\n\ttx.Add(ops)\n\tdrawDisc(ops, discDia, rgb(0x54514d))\n\ttx.Add(ops)\n\tdrawDisc(ops, discDia, rgb(0x54514d))\n\trow.Pop()\n\n\tty.Add(ops)\n\t// Second row.\n\trow = op.Offset(f32.Point{}).Push(ops)\n\tdrawDisc(ops, discDia, rgb(0xfffdfa))\n\ttx.Add(ops)\n\tdrawDisc(ops, discDia, rgb(0xfffdfa))\n\ttx.Add(ops)\n\tdrawDisc(ops, discDia, rgb(0xfffdfa))\n\trow.Pop()\n\n\tty.Add(ops)\n\t// Third row.\n\trow = op.Offset(f32.Point{}).Push(ops)\n\tdrawDisc(ops, discDia, rgb(0xfffdfa))\n\tdrawDisc(ops, discDia, rgb(0x54514d))\n\ttx.Add(ops)\n\tdrawDisc(ops, discDia, rgb(0xfffdfa))\n\ttx.Add(ops)\n\tdrawDisc(ops, discDia, rgb(0x54514d))\n\trow.Pop()\n}\n\nfunc drawImage(gtx layout.Context, img paint.ImageOp, size unit.Value) layout.Dimensions {\n\timg.Add(gtx.Ops)\n\tsz := img.Size()\n\taspect := float32(sz.Y) / float32(sz.X)\n\tw := gtx.Px(size)\n\th := int(float32(w)*aspect + .5)\n\tscale := float32(w) / float32(sz.X)\n\tdefer op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Point{X: scale, Y: scale})).Push(gtx.Ops).Pop()\n\tpaint.PaintOp{}.Add(gtx.Ops)\n\treturn layout.Dimensions{Size: image.Pt(w, h)}\n}\n\nfunc drawDisc(ops *op.Ops, radius float32, col color.NRGBA) {\n\tr2 := radius * .5\n\tdr := f32.Rectangle{Max: f32.Pt(radius, radius)}\n\tdefer clip.RRect{\n\t\tRect: dr,\n\t\tNE:   r2, NW: r2, SE: r2, SW: r2,\n\t}.Push(ops).Pop()\n\tpaint.ColorOp{Color: col}.Add(ops)\n\tpaint.PaintOp{}.Add(ops)\n}\n\n// Background lays out a widget and draws a color background behind it.\ntype Background struct {\n\tColor        color.NRGBA\n\tCornerRadius unit.Value\n}\n\nfunc (b Background) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {\n\tm := op.Record(gtx.Ops)\n\tdims := w(gtx)\n\tsz := dims.Size\n\tcall := m.Stop()\n\t// Clip corners, if any.\n\tif r := gtx.Px(b.CornerRadius); r > 0 {\n\t\trr := float32(r)\n\t\tdefer clip.RRect{\n\t\t\tRect: f32.Rectangle{Max: f32.Point{\n\t\t\t\tX: float32(sz.X),\n\t\t\t\tY: float32(sz.Y),\n\t\t\t}},\n\t\t\tNE: rr, NW: rr, SE: rr, SW: rr,\n\t\t}.Push(gtx.Ops).Pop()\n\t}\n\tfill{b.Color}.Layout(gtx, sz)\n\tcall.Add(gtx.Ops)\n\treturn dims\n}\n\ntype fill struct {\n\tcol color.NRGBA\n}\n\nfunc (f fill) Layout(gtx layout.Context, sz image.Point) layout.Dimensions {\n\tdefer clip.Rect(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop()\n\tpaint.ColorOp{Color: f.col}.Add(gtx.Ops)\n\tpaint.PaintOp{}.Add(gtx.Ops)\n\treturn layout.Dimensions{Size: sz}\n}\n\nfunc rgb(c uint32) color.NRGBA {\n\treturn argb((0xff << 24) | c)\n}\n\nfunc argb(c uint32) color.NRGBA {\n\treturn color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)}\n}\n\nconst termsText = `Tailscale is a mesh VPN for securely connecting your devices. All connections are device-to-device, so we never see your data.\n\nWe 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.`\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"Tailscale build environment\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs\";\n    android.url = \"github:tadfisher/android-nixpkgs\";\n    android.inputs.nixpkgs.follows = \"nixpkgs\";\n  };\n\n  outputs = { self, nixpkgs, android }:\n    let\n      supportedSystems = [ \"x86_64-linux\" \"x86_64-darwin\" \"aarch64-darwin\" ];\n      forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);\n    in\n    {\n      devShells = forAllSystems\n        (system:\n          let\n            pkgs = import nixpkgs {\n              inherit system;\n            };\n            android-sdk = android.sdk.${system} (sdkPkgs: with sdkPkgs;\n              [\n                build-tools-30-0-2\n                cmdline-tools-latest\n                platform-tools\n                platforms-android-31\n                platforms-android-30\n                ndk-23-1-7779620\n                patcher-v4\n              ]);\n          in\n          {\n            default = (with pkgs; buildFHSUserEnv {\n              name = \"tailscale\";\n              profile = ''\n                export ANDROID_SDK_ROOT=\"${android-sdk}/share/android-sdk\"\n                export JAVA_HOME=\"${jdk8.home}\"\n              '';\n              targetPkgs = pkgs: with pkgs; [\n                android-sdk\n                jdk8\n                clang\n              ] ++ (if stdenv.isLinux then [\n                vulkan-headers\n                libxkbcommon\n                wayland\n                xorg.libX11\n                xorg.libXcursor\n                xorg.libXfixes\n                libGL\n                pkgconfig\n              ] else [ ]);\n            }).env;\n          }\n        );\n    };\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/tailscale/tailscale-android\n\ngo 1.18\n\nrequire (\n\teliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3\n\tgioui.org v0.0.0-20220331105829-a1b5ff059c07\n\tgioui.org/cmd v0.0.0-20210925100615-41f3a7e74ee6\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e\n\tgolang.org/x/exp v0.0.0-20210722180016-6781d3edade3\n\tgolang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d\n\tgolang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478\n\tinet.af/netaddr v0.0.0-20220617031823-097006376321\n\ttailscale.com v1.1.1-0.20220718172352-3c892d106c2e\n)\n\nrequire (\n\tgioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect\n\tgioui.org/shader v1.0.6 // indirect\n\tgithub.com/akavel/rsrc v0.10.1 // indirect\n\tgithub.com/akutz/memconn v0.1.0 // indirect\n\tgithub.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect\n\tgithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect\n\tgithub.com/benoitkugler/textlayout v0.0.10 // indirect\n\tgithub.com/coreos/go-iptables v0.6.0 // indirect\n\tgithub.com/creack/pty v1.1.17 // indirect\n\tgithub.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 // indirect\n\tgithub.com/go-ole/go-ole v1.2.6 // indirect\n\tgithub.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506 // indirect\n\tgithub.com/godbus/dbus/v5 v5.0.6 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/google/btree v1.0.1 // indirect\n\tgithub.com/google/go-cmp v0.5.8 // indirect\n\tgithub.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e // indirect\n\tgithub.com/josharian/native v1.0.0 // indirect\n\tgithub.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect\n\tgithub.com/klauspost/compress v1.15.4 // indirect\n\tgithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect\n\tgithub.com/mdlayher/genetlink v1.2.0 // indirect\n\tgithub.com/mdlayher/netlink v1.6.0 // indirect\n\tgithub.com/mdlayher/sdnotify v1.0.0 // indirect\n\tgithub.com/mdlayher/socket v0.2.3 // indirect\n\tgithub.com/mitchellh/go-ps v1.0.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect\n\tgithub.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 // indirect\n\tgithub.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect\n\tgithub.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect\n\tgithub.com/tcnksm/go-httpstat v0.2.0 // indirect\n\tgithub.com/u-root/u-root v0.8.0 // indirect\n\tgithub.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 // indirect\n\tgithub.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 // indirect\n\tgithub.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect\n\tgo4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect\n\tgo4.org/mem v0.0.0-20210711025021-927187094b94 // indirect\n\tgo4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect\n\tgolang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect\n\tgolang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect\n\tgolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect\n\tgolang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect\n\tgolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect\n\tgolang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect\n\tgolang.org/x/text v0.3.7 // indirect\n\tgolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect\n\tgolang.org/x/tools v0.1.11 // indirect\n\tgolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect\n\tgolang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect\n\tgolang.zx2c4.com/wireguard/windows v0.4.10 // indirect\n\tgvisor.dev/gvisor v0.0.0-20220407223209-21871174d445 // indirect\n\tnhooyr.io/websocket v1.8.7 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\neliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 h1:djFprmHZgrSepsHAIRMp5UJn3PzsoTg9drI+BDmif5Q=\neliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=\nfilippo.io/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o=\ngioui.org v0.0.0-20210910062418-d5d0a75a9bcb/go.mod h1:BTldRXnY5mrUrYZCdWyDwyMzyUzpfZN1cF4MMRrOt9w=\ngioui.org v0.0.0-20220331105829-a1b5ff059c07 h1:9BdOhoKqRiItOl87wZfQUL2YZNm+6Yyp1f1iEiJUh+U=\ngioui.org v0.0.0-20220331105829-a1b5ff059c07/go.mod h1:b8vBukexG6eYuXZa14asjLAWJ+JjbZ/ophEnS2FjYUg=\ngioui.org/cmd v0.0.0-20210925100615-41f3a7e74ee6 h1:SkAdohDhTUjl+ZtM417Xeu+uFd7SQubwR9uAyqJqC8c=\ngioui.org/cmd v0.0.0-20210925100615-41f3a7e74ee6/go.mod h1:qrH3h4nt/PyIqx/XabL/eJ5cXQnQ0ERHqC3VEXx/Rmg=\ngioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=\ngioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=\ngioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=\ngioui.org/shader v1.0.2/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=\ngioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=\ngioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=\ngithub.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=\ngithub.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=\ngithub.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=\ngithub.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=\ngithub.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=\ngithub.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=\ngithub.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=\ngithub.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=\ngithub.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=\ngithub.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=\ngithub.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=\ngithub.com/benoitkugler/textlayout v0.0.5/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=\ngithub.com/benoitkugler/textlayout v0.0.10 h1:uIaQgH4pBFw1LQ0tPkfjgxo94WYcckzzQaB41L2X84w=\ngithub.com/benoitkugler/textlayout v0.0.10/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=\ngithub.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=\ngithub.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=\ngithub.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=\ngithub.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=\ngithub.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=\ngithub.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k=\ngithub.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=\ngithub.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao=\ngithub.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=\ngithub.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=\ngithub.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=\ngithub.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=\ngithub.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=\ngithub.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=\ngithub.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=\ngithub.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=\ngithub.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=\ngithub.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=\ngithub.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=\ngithub.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 h1:1bjaB/5IIicfKpP4k0s30T2WEw//Kh00zULa8DQ0cxA=\ngithub.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12/go.mod h1:kDhBRTA/i3H46PVdhqcw26TdGSIj42TOKNWKY+Kipnw=\ngithub.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=\ngithub.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=\ngithub.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506 h1:1TPz/Gn/MsXwJ6bEtI9wdkPcQYr2X3V9I+wz4wPYUdY=\ngithub.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506/go.mod h1:R0mlTNeyszZ/tKQhbZA7SRGjx+OHsmNzgN2jTV7yZcs=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=\ngithub.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=\ngithub.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=\ngithub.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=\ngithub.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=\ngithub.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=\ngithub.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=\ngithub.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=\ngithub.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=\ngithub.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=\ngithub.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e h1:IQpunlq7T+NiJJMO7ODYV2YWBiv/KnObR3gofX0mWOo=\ngithub.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=\ngithub.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291 h1:0J2ntV09uHLUHC79Z3YKJX2EnfOKL2QkMuHabu4L8JM=\ngithub.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291/go.mod h1:J7jazXS6RFR/oZT8XdfdD2KQ1bl56ukeE1qt4w8UQaI=\ngithub.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6kZr2O7PPdT+RkbSmmOponA8i/1DuGHe8BRsM=\ngithub.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ=\ngithub.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=\ngithub.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=\ngithub.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=\ngithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=\ngithub.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=\ngithub.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=\ngithub.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=\ngithub.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=\ngithub.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=\ngithub.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=\ngithub.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=\ngithub.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60/go.mod h1:aYbhishWc4Ai3I2U4Gaa2n3kHWSwzme6EsG/46HRQbE=\ngithub.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=\ngithub.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=\ngithub.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=\ngithub.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=\ngithub.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=\ngithub.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=\ngithub.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=\ngithub.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=\ngithub.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=\ngithub.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=\ngithub.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=\ngithub.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=\ngithub.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=\ngithub.com/mdlayher/netlink v1.4.2/go.mod h1:13VaingaArGUTUxFLf/iEovKxXji32JAtF858jZYEug=\ngithub.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=\ngithub.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=\ngithub.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=\ngithub.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=\ngithub.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697 h1:PBb7ld5cQGfxHF2pKvb/ydtuPwdRaltGI4e0QSCuiNI=\ngithub.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=\ngithub.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=\ngithub.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=\ngithub.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc=\ngithub.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g=\ngithub.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g=\ngithub.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI=\ngithub.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=\ngithub.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM=\ngithub.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=\ngithub.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=\ngithub.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=\ngithub.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=\ngithub.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=\ngithub.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=\ngithub.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=\ngithub.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=\ngithub.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=\ngithub.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=\ngithub.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=\ngithub.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=\ngithub.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=\ngithub.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8=\ngithub.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220326011347-d690bbfb6b5f h1:SO0bJlfWstNuolA3zjWDcLq0mjLfIw6RWEImAPxCkSU=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220326011347-d690bbfb6b5f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf h1:+DSoknr7gaiW2LlViX6+ko8TBdxTLkvOBbIWQtYyMaE=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220420170900-3a580d9e7b34 h1:ibRgOygS0bLOht+rOfaTuTvHiwmqeteG+rlFYs18aD8=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220420170900-3a580d9e7b34/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f h1:3CuODoSnBXS+ZkQlGakDqtX1o2RteR1870yF+dS61PY=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=\ngithub.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=\ngithub.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=\ngithub.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=\ngithub.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=\ngithub.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=\ngithub.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/u-root/u-root v0.8.0 h1:jqP7uPC2+0eRszYTrmdZ6UDyO1Dbuy0rpMo+BnPZ9cY=\ngithub.com/u-root/u-root v0.8.0/go.mod h1:But1FHzS4Ua4ywx6kZOaRzZTucUKIDKOPOLEKOckQ68=\ngithub.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=\ngithub.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 h1:XMAtQHwKjWHIRwg+8Nj/rzUomQY1q6cM3ncA0wP8GU4=\ngithub.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=\ngithub.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So=\ngithub.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=\ngithub.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=\ngithub.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngo.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=\ngo.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=\ngo.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=\ngo.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=\ngo.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=\ngo.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=\ngo.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=\ngo4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=\ngo4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=\ngo4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4=\ngo4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=\ngo4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA=\ngo4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=\ngo4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=\ngo4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=\ngolang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=\ngolang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=\ngolang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=\ngolang.org/x/exp v0.0.0-20210722180016-6781d3edade3 h1:IlrJD2AM5p8JhN/wVny9jt6gJ9hut2VALhSeZ3SYluk=\ngolang.org/x/exp v0.0.0-20210722180016-6781d3edade3/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc=\ngolang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM=\ngolang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb h1:fP6C8Xutcp5AlakmT/SkQot0pMicROAsEX7OfNPuG10=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=\ngolang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=\ngolang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=\ngolang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=\ngolang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220516155154-20f960328961 h1:+W/iTMPG0EL7aW+/atntZwZrvSRIj3m3yX414dSULUU=\ngolang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=\ngolang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=\ngolang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=\ngolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=\ngolang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=\ngolang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY=\ngolang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s=\ngolang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=\ngolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=\ngolang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=\ngolang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=\ngolang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1 h1:Z3vE1sGlC7qiyFJkkDcZms8Y3+yV8+W7HmDSmuf71tM=\ngolang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=\ngolang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=\ngolang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=\ngolang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=\ngolang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=\ngolang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961 h1:oIXcKhP1Ge6cRqdpQuldl0hf4mjIsNaXojabghlHuTs=\ngolang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=\ngolang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 h1:vDy//hdR+GnROE3OdYbQKt9rdtNdHkDtONvpRwmls/0=\ngolang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=\ngolang.zx2c4.com/wireguard/windows v0.4.10 h1:HmjzJnb+G4NCdX+sfjsQlsxGPuYaThxRbZUZFLyR0/s=\ngolang.zx2c4.com/wireguard/windows v0.4.10/go.mod h1:v7w/8FC48tTBm1IzScDVPEEb0/GjLta+T0ybpP9UWRg=\ngoogle.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=\ngvisor.dev/gvisor v0.0.0-20220318082524-536b85ae1a6a h1:sQAuNyyy59GRxS8npo8nyOr5yM46gB7QzVFiq6yvHdg=\ngvisor.dev/gvisor v0.0.0-20220318082524-536b85ae1a6a/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI=\ngvisor.dev/gvisor v0.0.0-20220407223209-21871174d445 h1:pLNQCtMzh4O6rdhoUeWHuutt4yMft+B9Cgw/bezWchE=\ngvisor.dev/gvisor v0.0.0-20220407223209-21871174d445/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI=\nhonnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=\nhonnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=\nhonnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=\nhonnef.co/go/tools v0.3.0-0.dev.0.20220306074811-23e1086441d2 h1:utiSabORbG/JeX7MlmKMdmsjwom2+v8zmdb6SoBe4UY=\nhonnef.co/go/tools v0.4.0-0.dev.0.20220404092545-59d7a2877f83 h1:lZ9GIYaU+o5+X6ST702I/Ntyq9Y2oIMZ42rBQpem64A=\nhowett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=\ninet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=\ninet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=\ninet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=\ninet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=\nnhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=\nnhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=\nsigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=\nsoftware.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4=\nsourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=\ntailscale.com v1.1.1-0.20220327044527-7c7f37342fa7 h1:NH+oDzd3mnPQWiOqLyBZTvNj9PFo1p2NAJ8G0aCMiIo=\ntailscale.com v1.1.1-0.20220327044527-7c7f37342fa7/go.mod h1:ptcVi35GenDhRsw+7hLsIGSmY0W2lsZGBEODHS301tQ=\ntailscale.com v1.1.1-0.20220330175050-9f604f2bd3b4 h1:snGTFNSURzgRkPsEBZMAZcmkHsQWYBXELIHcnslLTk4=\ntailscale.com v1.1.1-0.20220330175050-9f604f2bd3b4/go.mod h1:JpNVbOb4vjyi/Ny9vgc8A0M8d6JQHC3RhyXS+6DOIhQ=\ntailscale.com v1.1.1-0.20220416053651-c591c9165379 h1:GKmD3oVdtPKERevIiqCuVZHVMwpdcdfzOq7RkJwJ3ys=\ntailscale.com v1.1.1-0.20220416053651-c591c9165379/go.mod h1:8UcUkPq+JvAmq/GYyeNRP/1aQwly/33dQu6IEU+lh/A=\ntailscale.com v1.1.1-0.20220419050352-c13be0c5090c h1:jN2RyioI34KnqpjobSeiH7V0drz4YU4v7mxADoUqz+I=\ntailscale.com v1.1.1-0.20220419050352-c13be0c5090c/go.mod h1:8UcUkPq+JvAmq/GYyeNRP/1aQwly/33dQu6IEU+lh/A=\ntailscale.com v1.1.1-0.20220421004349-13f75b9667c0 h1:zs3cQ6D/PFUuGq+cRi1u0S4+3Sbz5A/Id5hzlmQtbbg=\ntailscale.com v1.1.1-0.20220421004349-13f75b9667c0/go.mod h1:YsCSd5p5D3M2mGwFDB+zZZrcFijecDsbjdiSfO6YAEM=\ntailscale.com v1.1.1-0.20220421175222-695f8a1d7e4d h1:lZxEfYG/4sLnGfbpizrD1fD4K6EjXsa4Jas8g5WVq8Y=\ntailscale.com v1.1.1-0.20220421175222-695f8a1d7e4d/go.mod h1:jqMyhjRwWu4Wzem9iVSSIjSsTMIeFXmY/Lr8a+UccbA=\ntailscale.com v1.1.1-0.20220423065216-bbca2c78cbd0 h1:A/Vhsbrl3xlznK4/lRZIFfp1GQKHX7b/8mT80OEFDpw=\ntailscale.com v1.1.1-0.20220423065216-bbca2c78cbd0/go.mod h1:5yjfQLg8gHScx/9S/SX0sim5sUKkk14HdATuAUHxGmA=\ntailscale.com v1.1.1-0.20220430025040-e3619b890c20 h1:NTk7u7/6JiaJV1dcOtqwPE+zREKmFfHnFK/wAFabA3Y=\ntailscale.com v1.1.1-0.20220430025040-e3619b890c20/go.mod h1:SjELlTggz8Lsgebu1kjfOoG+ZpYAr6t8qaxohrIPSLk=\ntailscale.com v1.1.1-0.20220506211610-741ae9956e67 h1:9kjMCMfJfTKJm53Ox9/1bQoLIvtYe2grCJG2d9Q00ms=\ntailscale.com v1.1.1-0.20220506211610-741ae9956e67/go.mod h1:2TUB66uN02iBFIH9XH3U8huzMuiulhMkWEBLOtP/L4I=\ntailscale.com v1.1.1-0.20220520203011-fc5839864bfb h1:ySbGnpei9LirCkocXjhGtNZlQqY8eb00rDOaAQh7k2g=\ntailscale.com v1.1.1-0.20220520203011-fc5839864bfb/go.mod h1:2affa2vYcxM5WnIlYtpkZsosVMZrrkF3oE5ZRx507MA=\ntailscale.com v1.1.1-0.20220618023759-467eb2eca09d h1:HOgwZUuK1POz5KS9bEWcEqxYRgFLa/9YqABNHfcYjso=\ntailscale.com v1.1.1-0.20220618023759-467eb2eca09d/go.mod h1:eTdHYI3iFv/1+rCxh60KyY2QjVToVuvbhAR9UxKuigM=\ntailscale.com v1.1.1-0.20220628235937-06aa14163254 h1:hNPKuQ95T48R2Myx46MOHZL+66XgCFEpShcNSN0SgMU=\ntailscale.com v1.1.1-0.20220628235937-06aa14163254/go.mod h1:eTdHYI3iFv/1+rCxh60KyY2QjVToVuvbhAR9UxKuigM=\ntailscale.com v1.1.1-0.20220706124106-e6572a0f088a h1:V7D27nnpKz2RHeluxS853jqUWSl+/FMQeCSG39otKvc=\ntailscale.com v1.1.1-0.20220706124106-e6572a0f088a/go.mod h1:sw/kpgrJDgUjz+aBNVCoyJa2Eksd9BbP9xusFYOc3MQ=\ntailscale.com v1.1.1-0.20220712041646-755396d6fe77 h1:1YQCCgvywvVuD3A1hzc4L9bYMJeG0kfbxtzZCIttbmU=\ntailscale.com v1.1.1-0.20220712041646-755396d6fe77/go.mod h1:bta0vbR8Cajmq+oq3as0/iAWWnWqnYv+cas4ftX9KAw=\ntailscale.com v1.1.1-0.20220718172352-3c892d106c2e h1:Mq8xSdoqp/UabuPt405owY20/mt3q5z7fSruQQzC9Z8=\ntailscale.com v1.1.1-0.20220718172352-3c892d106c2e/go.mod h1:T9uKhlkxVPdSu1Qvp882evcS/hQ1+TAyZ7sJ/VACGRI=\n"
  },
  {
    "path": "jni/jni.go",
    "content": "// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package jni implements various helper functions for communicating with the Android JVM\n// though JNI.\npackage jni\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"sync\"\n\t\"unicode/utf16\"\n\t\"unsafe\"\n)\n\n/*\n#cgo CFLAGS: -Wall\n\n#include <jni.h>\n#include <stdlib.h>\n\nstatic jint jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) {\n\treturn (*vm)->AttachCurrentThread(vm, p_env, thr_args);\n}\n\nstatic jint jni_DetachCurrentThread(JavaVM *vm) {\n\treturn (*vm)->DetachCurrentThread(vm);\n}\n\nstatic jint jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) {\n\treturn (*vm)->GetEnv(vm, (void **)env, version);\n}\n\nstatic jclass jni_FindClass(JNIEnv *env, const char *name) {\n\treturn (*env)->FindClass(env, name);\n}\n\nstatic jthrowable jni_ExceptionOccurred(JNIEnv *env) {\n\treturn (*env)->ExceptionOccurred(env);\n}\n\nstatic void jni_ExceptionClear(JNIEnv *env) {\n\t(*env)->ExceptionClear(env);\n}\n\nstatic jclass jni_GetObjectClass(JNIEnv *env, jobject obj) {\n\treturn (*env)->GetObjectClass(env, obj);\n}\n\nstatic jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {\n\treturn (*env)->GetMethodID(env, clazz, name, sig);\n}\n\nstatic jmethodID jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {\n\treturn (*env)->GetStaticMethodID(env, clazz, name, sig);\n}\n\nstatic jsize jni_GetStringLength(JNIEnv *env, jstring str) {\n\treturn (*env)->GetStringLength(env, str);\n}\n\nstatic const jchar *jni_GetStringChars(JNIEnv *env, jstring str) {\n\treturn (*env)->GetStringChars(env, str, NULL);\n}\n\nstatic jstring jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) {\n\treturn (*env)->NewString(env, unicodeChars, len);\n}\n\nstatic jboolean jni_IsSameObject(JNIEnv *env, jobject ref1, jobject ref2) {\n\treturn (*env)->IsSameObject(env, ref1, ref2);\n}\n\nstatic jobject jni_NewGlobalRef(JNIEnv *env, jobject obj) {\n\treturn (*env)->NewGlobalRef(env, obj);\n}\n\nstatic void jni_DeleteGlobalRef(JNIEnv *env, jobject obj) {\n\t(*env)->DeleteGlobalRef(env, obj);\n}\n\nstatic void jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {\n\t(*env)->CallStaticVoidMethodA(env, cls, method, args);\n}\n\nstatic jint jni_CallStaticIntMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {\n\treturn (*env)->CallStaticIntMethodA(env, cls, method, args);\n}\n\nstatic jobject jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {\n\treturn (*env)->CallStaticObjectMethodA(env, cls, method, args);\n}\n\nstatic jobject jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {\n\treturn (*env)->CallObjectMethodA(env, obj, method, args);\n}\n\nstatic jboolean jni_CallBooleanMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {\n\treturn (*env)->CallBooleanMethodA(env, obj, method, args);\n}\n\nstatic jint jni_CallIntMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {\n\treturn (*env)->CallIntMethodA(env, obj, method, args);\n}\n\nstatic void jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {\n\t(*env)->CallVoidMethodA(env, obj, method, args);\n}\n\nstatic jbyteArray jni_NewByteArray(JNIEnv *env, jsize length) {\n\treturn (*env)->NewByteArray(env, length);\n}\n\nstatic jboolean *jni_GetBooleanArrayElements(JNIEnv *env, jbooleanArray arr) {\n\treturn (*env)->GetBooleanArrayElements(env, arr, NULL);\n}\n\nstatic void jni_ReleaseBooleanArrayElements(JNIEnv *env, jbooleanArray arr, jboolean *elems, jint mode) {\n\t(*env)->ReleaseBooleanArrayElements(env, arr, elems, mode);\n}\n\nstatic jbyte *jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) {\n\treturn (*env)->GetByteArrayElements(env, arr, NULL);\n}\n\nstatic jint *jni_GetIntArrayElements(JNIEnv *env, jintArray arr) {\n\treturn (*env)->GetIntArrayElements(env, arr, NULL);\n}\n\nstatic void jni_ReleaseIntArrayElements(JNIEnv *env, jintArray arr, jint *elems, jint mode) {\n\t(*env)->ReleaseIntArrayElements(env, arr, elems, mode);\n}\n\nstatic jlong *jni_GetLongArrayElements(JNIEnv *env, jlongArray arr) {\n\treturn (*env)->GetLongArrayElements(env, arr, NULL);\n}\n\nstatic void jni_ReleaseLongArrayElements(JNIEnv *env, jlongArray arr, jlong *elems, jint mode) {\n\t(*env)->ReleaseLongArrayElements(env, arr, elems, mode);\n}\n\nstatic void jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *elems, jint mode) {\n\t(*env)->ReleaseByteArrayElements(env, arr, elems, mode);\n}\n\nstatic jsize jni_GetArrayLength(JNIEnv *env, jarray arr) {\n\treturn (*env)->GetArrayLength(env, arr);\n}\n\nstatic void jni_DeleteLocalRef(JNIEnv *env, jobject localRef) {\n\treturn (*env)->DeleteLocalRef(env, localRef);\n}\n\nstatic jobject jni_GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index) {\n\treturn (*env)->GetObjectArrayElement(env, array, index);\n}\n\nstatic jboolean jni_IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz) {\n\treturn (*env)->IsInstanceOf(env, obj, clazz);\n}\n*/\nimport \"C\"\n\ntype JVM C.JavaVM\n\ntype Env C.JNIEnv\n\ntype (\n\tClass        C.jclass\n\tObject       C.jobject\n\tMethodID     C.jmethodID\n\tString       C.jstring\n\tByteArray    C.jbyteArray\n\tObjectArray  C.jobjectArray\n\tBooleanArray C.jbooleanArray\n\tLongArray    C.jlongArray\n\tIntArray     C.jintArray\n\tBoolean      C.jboolean\n\tValue        uint64 // All JNI types fit into 64-bits.\n)\n\n// Cached class handles.\nvar classes struct {\n\tonce                      sync.Once\n\tstringClass, integerClass Class\n\n\tintegerIntValue MethodID\n}\n\nfunc env(e *Env) *C.JNIEnv {\n\treturn (*C.JNIEnv)(unsafe.Pointer(e))\n}\n\nfunc javavm(vm *JVM) *C.JavaVM {\n\treturn (*C.JavaVM)(unsafe.Pointer(vm))\n}\n\n// Do invokes a function with a temporary JVM environment. The\n// environment is not valid after the function returns.\nfunc Do(vm *JVM, f func(env *Env) error) error {\n\truntime.LockOSThread()\n\tdefer runtime.UnlockOSThread()\n\tvar env *C.JNIEnv\n\tif res := C.jni_GetEnv(javavm(vm), &env, C.JNI_VERSION_1_6); res != C.JNI_OK {\n\t\tif res != C.JNI_EDETACHED {\n\t\t\tpanic(fmt.Errorf(\"JNI GetEnv failed with error %d\", res))\n\t\t}\n\t\tif C.jni_AttachCurrentThread(javavm(vm), &env, nil) != C.JNI_OK {\n\t\t\tpanic(errors.New(\"runInJVM: AttachCurrentThread failed\"))\n\t\t}\n\t\tdefer C.jni_DetachCurrentThread(javavm(vm))\n\t}\n\n\treturn f((*Env)(unsafe.Pointer(env)))\n}\n\nfunc Bool(b bool) Boolean {\n\tif b {\n\t\treturn C.JNI_TRUE\n\t}\n\treturn C.JNI_FALSE\n}\n\nfunc varArgs(args []Value) *C.jvalue {\n\tif len(args) == 0 {\n\t\treturn nil\n\t}\n\treturn (*C.jvalue)(unsafe.Pointer(&args[0]))\n}\n\nfunc IsSameObject(e *Env, ref1, ref2 Object) bool {\n\tsame := C.jni_IsSameObject(env(e), C.jobject(ref1), C.jobject(ref2))\n\treturn same == C.JNI_TRUE\n}\n\nfunc CallStaticIntMethod(e *Env, cls Class, method MethodID, args ...Value) (int, error) {\n\tres := C.jni_CallStaticIntMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args))\n\treturn int(res), exception(e)\n}\n\nfunc CallStaticVoidMethod(e *Env, cls Class, method MethodID, args ...Value) error {\n\tC.jni_CallStaticVoidMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args))\n\treturn exception(e)\n}\n\nfunc CallVoidMethod(e *Env, obj Object, method MethodID, args ...Value) error {\n\tC.jni_CallVoidMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))\n\treturn exception(e)\n}\n\nfunc CallStaticObjectMethod(e *Env, cls Class, method MethodID, args ...Value) (Object, error) {\n\tres := C.jni_CallStaticObjectMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args))\n\treturn Object(res), exception(e)\n}\n\nfunc CallObjectMethod(e *Env, obj Object, method MethodID, args ...Value) (Object, error) {\n\tres := C.jni_CallObjectMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))\n\treturn Object(res), exception(e)\n}\n\nfunc CallBooleanMethod(e *Env, obj Object, method MethodID, args ...Value) (bool, error) {\n\tres := C.jni_CallBooleanMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))\n\treturn res == C.JNI_TRUE, exception(e)\n}\n\nfunc CallIntMethod(e *Env, obj Object, method MethodID, args ...Value) (int32, error) {\n\tres := C.jni_CallIntMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))\n\treturn int32(res), exception(e)\n}\n\n// GetByteArrayElements returns the contents of the byte array.\nfunc GetByteArrayElements(e *Env, jarr ByteArray) []byte {\n\tif jarr == 0 {\n\t\treturn nil\n\t}\n\tsize := C.jni_GetArrayLength(env(e), C.jarray(jarr))\n\telems := C.jni_GetByteArrayElements(env(e), C.jbyteArray(jarr))\n\tdefer C.jni_ReleaseByteArrayElements(env(e), C.jbyteArray(jarr), elems, 0)\n\tbacking := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:size:size]\n\ts := make([]byte, len(backing))\n\tcopy(s, backing)\n\treturn s\n}\n\n// GetBooleanArrayElements returns the contents of the boolean array.\nfunc GetBooleanArrayElements(e *Env, jarr BooleanArray) []bool {\n\tif jarr == 0 {\n\t\treturn nil\n\t}\n\tsize := C.jni_GetArrayLength(env(e), C.jarray(jarr))\n\telems := C.jni_GetBooleanArrayElements(env(e), C.jbooleanArray(jarr))\n\tdefer C.jni_ReleaseBooleanArrayElements(env(e), C.jbooleanArray(jarr), elems, 0)\n\tbacking := (*(*[1 << 30]C.jboolean)(unsafe.Pointer(elems)))[:size:size]\n\tr := make([]bool, len(backing))\n\tfor i, b := range backing {\n\t\tr[i] = b == C.JNI_TRUE\n\t}\n\treturn r\n}\n\n// GetStringArrayElements returns the contents of the String array.\nfunc GetStringArrayElements(e *Env, jarr ObjectArray) []string {\n\tvar strings []string\n\titerateObjectArray(e, jarr, func(e *Env, idx int, item Object) {\n\t\ts := GoString(e, String(item))\n\t\tstrings = append(strings, s)\n\t})\n\treturn strings\n}\n\n// GetIntArrayElements returns the contents of the int array.\nfunc GetIntArrayElements(e *Env, jarr IntArray) []int {\n\tif jarr == 0 {\n\t\treturn nil\n\t}\n\tsize := C.jni_GetArrayLength(env(e), C.jarray(jarr))\n\telems := C.jni_GetIntArrayElements(env(e), C.jintArray(jarr))\n\tdefer C.jni_ReleaseIntArrayElements(env(e), C.jintArray(jarr), elems, 0)\n\tbacking := (*(*[1 << 27]C.jint)(unsafe.Pointer(elems)))[:size:size]\n\tr := make([]int, len(backing))\n\tfor i, l := range backing {\n\t\tr[i] = int(l)\n\t}\n\treturn r\n}\n\n// GetLongArrayElements returns the contents of the long array.\nfunc GetLongArrayElements(e *Env, jarr LongArray) []int64 {\n\tif jarr == 0 {\n\t\treturn nil\n\t}\n\tsize := C.jni_GetArrayLength(env(e), C.jarray(jarr))\n\telems := C.jni_GetLongArrayElements(env(e), C.jlongArray(jarr))\n\tdefer C.jni_ReleaseLongArrayElements(env(e), C.jlongArray(jarr), elems, 0)\n\tbacking := (*(*[1 << 27]C.jlong)(unsafe.Pointer(elems)))[:size:size]\n\tr := make([]int64, len(backing))\n\tfor i, l := range backing {\n\t\tr[i] = int64(l)\n\t}\n\treturn r\n}\n\nfunc iterateObjectArray(e *Env, jarr ObjectArray, f func(e *Env, idx int, item Object)) {\n\tif jarr == 0 {\n\t\treturn\n\t}\n\tsize := C.jni_GetArrayLength(env(e), C.jarray(jarr))\n\tfor i := 0; i < int(size); i++ {\n\t\titem := C.jni_GetObjectArrayElement(env(e), C.jobjectArray(jarr), C.jint(i))\n\t\tf(e, i, Object(item))\n\t\tC.jni_DeleteLocalRef(env(e), item)\n\t}\n}\n\n// NewByteArray allocates a Java byte array with the content. It\n// panics if the allocation fails.\nfunc NewByteArray(e *Env, content []byte) ByteArray {\n\tjarr := C.jni_NewByteArray(env(e), C.jsize(len(content)))\n\tif jarr == 0 {\n\t\tpanic(fmt.Errorf(\"jni: NewByteArray(%d) failed\", len(content)))\n\t}\n\telems := C.jni_GetByteArrayElements(env(e), jarr)\n\tdefer C.jni_ReleaseByteArrayElements(env(e), jarr, elems, 0)\n\tbacking := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:len(content):len(content)]\n\tcopy(backing, content)\n\treturn ByteArray(jarr)\n}\n\n// ClassLoader returns a reference to the Java ClassLoader associated\n// with obj.\nfunc ClassLoaderFor(e *Env, obj Object) Object {\n\tcls := GetObjectClass(e, obj)\n\tgetClassLoader := GetMethodID(e, cls, \"getClassLoader\", \"()Ljava/lang/ClassLoader;\")\n\tclsLoader, err := CallObjectMethod(e, Object(obj), getClassLoader)\n\tif err != nil {\n\t\t// Class.getClassLoader should never fail.\n\t\tpanic(err)\n\t}\n\treturn Object(clsLoader)\n}\n\n// LoadClass invokes the underlying ClassLoader's loadClass method and\n// returns the class.\nfunc LoadClass(e *Env, loader Object, class string) (Class, error) {\n\tcls := GetObjectClass(e, loader)\n\tloadClass := GetMethodID(e, cls, \"loadClass\", \"(Ljava/lang/String;)Ljava/lang/Class;\")\n\tname := JavaString(e, class)\n\tloaded, err := CallObjectMethod(e, loader, loadClass, Value(name))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn Class(loaded), exception(e)\n}\n\n// exception returns an error corresponding to the pending\n// exception, and clears it. exceptionError returns nil if no\n// exception is pending.\nfunc exception(e *Env) error {\n\tthr := C.jni_ExceptionOccurred(env(e))\n\tif thr == 0 {\n\t\treturn nil\n\t}\n\tC.jni_ExceptionClear(env(e))\n\tcls := GetObjectClass(e, Object(thr))\n\ttoString := GetMethodID(e, cls, \"toString\", \"()Ljava/lang/String;\")\n\tmsg, err := CallObjectMethod(e, Object(thr), toString)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn errors.New(GoString(e, String(msg)))\n}\n\n// GetObjectClass returns the Java Class for an Object.\nfunc GetObjectClass(e *Env, obj Object) Class {\n\tif obj == 0 {\n\t\tpanic(\"null object\")\n\t}\n\tcls := C.jni_GetObjectClass(env(e), C.jobject(obj))\n\tif err := exception(e); err != nil {\n\t\t// GetObjectClass should never fail.\n\t\tpanic(err)\n\t}\n\treturn Class(cls)\n}\n\n// GetStaticMethodID returns the id for a static method. It panics if the method\n// wasn't found.\nfunc GetStaticMethodID(e *Env, cls Class, name, signature string) MethodID {\n\tmname := C.CString(name)\n\tdefer C.free(unsafe.Pointer(mname))\n\tmsig := C.CString(signature)\n\tdefer C.free(unsafe.Pointer(msig))\n\tm := C.jni_GetStaticMethodID(env(e), C.jclass(cls), mname, msig)\n\tif err := exception(e); err != nil {\n\t\tpanic(err)\n\t}\n\treturn MethodID(m)\n}\n\n// GetMethodID returns the id for a method. It panics if the method\n// wasn't found.\nfunc GetMethodID(e *Env, cls Class, name, signature string) MethodID {\n\tmname := C.CString(name)\n\tdefer C.free(unsafe.Pointer(mname))\n\tmsig := C.CString(signature)\n\tdefer C.free(unsafe.Pointer(msig))\n\tm := C.jni_GetMethodID(env(e), C.jclass(cls), mname, msig)\n\tif err := exception(e); err != nil {\n\t\tpanic(err)\n\t}\n\treturn MethodID(m)\n}\n\nfunc NewGlobalRef(e *Env, obj Object) Object {\n\treturn Object(C.jni_NewGlobalRef(env(e), C.jobject(obj)))\n}\n\nfunc DeleteGlobalRef(e *Env, obj Object) {\n\tC.jni_DeleteGlobalRef(env(e), C.jobject(obj))\n}\n\n// JavaString converts the string to a JVM jstring.\nfunc JavaString(e *Env, str string) String {\n\tif str == \"\" {\n\t\treturn 0\n\t}\n\tutf16Chars := utf16.Encode([]rune(str))\n\tres := C.jni_NewString(env(e), (*C.jchar)(unsafe.Pointer(&utf16Chars[0])), C.int(len(utf16Chars)))\n\treturn String(res)\n}\n\n// GoString converts the JVM jstring to a Go string.\nfunc GoString(e *Env, str String) string {\n\tif str == 0 {\n\t\treturn \"\"\n\t}\n\tstrlen := C.jni_GetStringLength(env(e), C.jstring(str))\n\tchars := C.jni_GetStringChars(env(e), C.jstring(str))\n\tvar utf16Chars []uint16\n\thdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars))\n\thdr.Data = uintptr(unsafe.Pointer(chars))\n\thdr.Cap = int(strlen)\n\thdr.Len = int(strlen)\n\tutf8 := utf16.Decode(utf16Chars)\n\treturn string(utf8)\n}\n"
  },
  {
    "path": "metadata/en-US/full_description.txt",
    "content": "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.\n\nWireGuard is a registered trademark of Jason A. Donenfeld.\n"
  },
  {
    "path": "metadata/en-US/short_description.txt",
    "content": "Mesh VPN based on WireGuard\n"
  },
  {
    "path": "version/tailscale-version.sh",
    "content": "#!/usr/bin/env bash\n\n# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.\n# Use of this source code is governed by a BSD-style\n# license that can be found in the LICENSE file.\n\n# Print the version tailscale repository corresponding\n# to the version listed in go.mod.\n\nset -euo pipefail\n\ngo_list=$(go list -m tailscale.com)\n# go list outputs `tailscale.com <version>`. Extract the version.\nmod_version=${go_list##* }\n\nif [ -z \"$mod_version\" ]; then\n\techo \"no version reported by go list -m tailscale.com: $go_list\"\n\texit 1\nfi\n\ncase \"$mod_version\" in\n\t*-*-*)\n\t\t# A pseudo-version such as \"v1.1.1-0.20201030135043-eab6e9ea4e45\"\n\t\t# includes the commit hash.\n\t\tmod_version=${mod_version##*-*-}\n\t\t;;\nesac\n\ntailscale_clone=$(mktemp -d -t tailscale-clone-XXXXXXXXXX)\ngit clone -q https://github.com/tailscale/tailscale.git \"$tailscale_clone\"\n\ncd $tailscale_clone\ngit reset --hard -q\ngit clean -d -x -f\ngit fetch -q --all --tags\ngit checkout -q \"$mod_version\"\n\neval $(./build_dist.sh shellvars)\ngit_hash=$(git rev-parse HEAD)\nshort_hash=$(echo \"$git_hash\" | cut -c1-9)\necho ${VERSION_SHORT}-t${short_hash}\ncd /tmp\nrm -rf \"$tailscale_clone\"\n"
  }
]