cmd/tailscale/cli: make "update" work on Windows
Updates #6995 Co-authored-by: Aaron Klotz <aaron@tailscale.com> Change-Id: I16622f43156a70b6fbc8205239fd489d7378d57b Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
4fb663fbd2
commit
71a7b8581d
38
cmd/tailscale/cli/authenticode_windows.go
Normal file
38
cmd/tailscale/cli/authenticode_windows.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
verifyAuthenticode = verifyAuthenticodeWindows
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyAuthenticodeWindows(path string) error {
|
||||||
|
path16, err := windows.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data := &windows.WinTrustData{
|
||||||
|
Size: uint32(unsafe.Sizeof(windows.WinTrustData{})),
|
||||||
|
UIChoice: windows.WTD_UI_NONE,
|
||||||
|
RevocationChecks: windows.WTD_REVOKE_WHOLECHAIN, // Full revocation checking, as this is called with network connectivity.
|
||||||
|
UnionChoice: windows.WTD_CHOICE_FILE,
|
||||||
|
StateAction: windows.WTD_STATEACTION_VERIFY,
|
||||||
|
FileOrCatalogOrBlobOrSgnrOrCert: unsafe.Pointer(&windows.WinTrustFileInfo{
|
||||||
|
Size: uint32(unsafe.Sizeof(windows.WinTrustFileInfo{})),
|
||||||
|
FilePath: path16,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
err = windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
|
||||||
|
data.StateAction = windows.WTD_STATEACTION_CLOSE
|
||||||
|
windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
|
||||||
|
return err
|
||||||
|
}
|
@ -5,17 +5,30 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/net/tshttpproxy"
|
||||||
|
"tailscale.com/util/must"
|
||||||
"tailscale.com/util/winutil"
|
"tailscale.com/util/winutil"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
@ -24,7 +37,7 @@
|
|||||||
var updateCmd = &ffcli.Command{
|
var updateCmd = &ffcli.Command{
|
||||||
Name: "update",
|
Name: "update",
|
||||||
ShortUsage: "update",
|
ShortUsage: "update",
|
||||||
ShortHelp: "Update Tailscale to the latest/different version",
|
ShortHelp: "[ALPHA] Update Tailscale to the latest/different version",
|
||||||
Exec: runUpdate,
|
Exec: runUpdate,
|
||||||
FlagSet: (func() *flag.FlagSet {
|
FlagSet: (func() *flag.FlagSet {
|
||||||
fs := newFlagSet("update")
|
fs := newFlagSet("update")
|
||||||
@ -43,7 +56,22 @@
|
|||||||
version string // explicit version; empty means auto
|
version string // explicit version; empty means auto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// winMSIEnv is the environment variable that, if set, is the MSI file for the
|
||||||
|
// update command to install. It's passed like this so we can stop the
|
||||||
|
// tailscale.exe process from running before the msiexec process runs and tries
|
||||||
|
// to overwrite ourselves.
|
||||||
|
const winMSIEnv = "TS_UPDATE_WIN_MSI"
|
||||||
|
|
||||||
func runUpdate(ctx context.Context, args []string) error {
|
func runUpdate(ctx context.Context, args []string) error {
|
||||||
|
if msi := os.Getenv(winMSIEnv); msi != "" {
|
||||||
|
log.Printf("installing %v ...", msi)
|
||||||
|
if err := installMSI(msi); err != nil {
|
||||||
|
log.Printf("MSI install failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("success.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
}
|
}
|
||||||
@ -57,6 +85,22 @@ func runUpdate(ctx context.Context, args []string) error {
|
|||||||
return up.update()
|
return up.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func versionIsStable(v string) (stable, wellFormed bool) {
|
||||||
|
_, rest, ok := strings.Cut(v, ".")
|
||||||
|
if !ok {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
minorStr, _, ok := strings.Cut(rest, ".")
|
||||||
|
if !ok {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
minor, err := strconv.Atoi(minorStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
return minor%2 == 0, true
|
||||||
|
}
|
||||||
|
|
||||||
func newUpdater() (*updater, error) {
|
func newUpdater() (*updater, error) {
|
||||||
up := &updater{
|
up := &updater{
|
||||||
track: updateArgs.track,
|
track: updateArgs.track,
|
||||||
@ -69,6 +113,17 @@ func newUpdater() (*updater, error) {
|
|||||||
} else {
|
} else {
|
||||||
up.track = "stable"
|
up.track = "stable"
|
||||||
}
|
}
|
||||||
|
if updateArgs.version != "" {
|
||||||
|
stable, ok := versionIsStable(updateArgs.version)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("malformed version %q", updateArgs.version)
|
||||||
|
}
|
||||||
|
if stable {
|
||||||
|
up.track = "stable"
|
||||||
|
} else {
|
||||||
|
up.track = "unstable"
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown track %q; must be 'stable' or 'unstable'", up.track)
|
return nil, fmt.Errorf("unknown track %q; must be 'stable' or 'unstable'", up.track)
|
||||||
}
|
}
|
||||||
@ -115,6 +170,23 @@ func (up *updater) currentOrDryRun(ver string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (up *updater) confirm(ver string) error {
|
||||||
|
if updateArgs.yes {
|
||||||
|
log.Printf("Updating Tailscale from %v to %v; --yes given, continuing without prompts.\n", version.Short, ver)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("This will update Tailscale from %v to %v. Continue? [y/n] ", version.Short, ver)
|
||||||
|
var resp string
|
||||||
|
fmt.Scanln(&resp)
|
||||||
|
resp = strings.ToLower(resp)
|
||||||
|
switch resp {
|
||||||
|
case "y", "yes", "sure":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("aborting update")
|
||||||
|
}
|
||||||
|
|
||||||
func (up *updater) updateSynology() error {
|
func (up *updater) updateSynology() error {
|
||||||
// TODO(bradfitz): detect, map GOARCH+CPU to the right Synology arch.
|
// TODO(bradfitz): detect, map GOARCH+CPU to the right Synology arch.
|
||||||
// TODO(bradfitz): add pkgs.tailscale.com endpoint to get release info
|
// TODO(bradfitz): add pkgs.tailscale.com endpoint to get release info
|
||||||
@ -168,6 +240,11 @@ func (up *updater) updateMacSys() error {
|
|||||||
return errors.New("The 'update' command is not yet implemented on macOS.")
|
return errors.New("The 'update' command is not yet implemented on macOS.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
verifyAuthenticode func(string) error // or nil on non-Windows
|
||||||
|
markTempFileFunc func(string) error // or nil on non-Windows
|
||||||
|
)
|
||||||
|
|
||||||
func (up *updater) updateWindows() error {
|
func (up *updater) updateWindows() error {
|
||||||
ver := updateArgs.version
|
ver := updateArgs.version
|
||||||
if ver == "" {
|
if ver == "" {
|
||||||
@ -200,6 +277,218 @@ func (up *updater) updateWindows() error {
|
|||||||
if !winutil.IsCurrentProcessElevated() {
|
if !winutil.IsCurrentProcessElevated() {
|
||||||
return errors.New("must be run as Administrator")
|
return errors.New("must be run as Administrator")
|
||||||
}
|
}
|
||||||
// TODO(bradfitz): require elevated mode
|
|
||||||
return errors.New("TODO: download + msiexec /i /quiet " + url)
|
tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
|
||||||
|
msiDir := filepath.Join(tsDir, "MSICache")
|
||||||
|
if fi, err := os.Stat(tsDir); err != nil {
|
||||||
|
return fmt.Errorf("expected %s to exist, got stat error: %w", tsDir, err)
|
||||||
|
} else if !fi.IsDir() {
|
||||||
|
return fmt.Errorf("expected %s to be a directory; got %v", tsDir, fi.Mode())
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(msiDir, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := up.confirm(ver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msiTarget := filepath.Join(msiDir, path.Base(url))
|
||||||
|
if err := downloadURLToFile(url, msiTarget); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("verifying MSI authenticode...")
|
||||||
|
if err := verifyAuthenticode(msiTarget); err != nil {
|
||||||
|
return fmt.Errorf("authenticode verification of %s failed: %w", msiTarget, err)
|
||||||
|
}
|
||||||
|
log.Printf("authenticode verification succeeded")
|
||||||
|
|
||||||
|
log.Printf("making tailscale.exe copy to switch to...")
|
||||||
|
selfCopy, err := makeSelfCopy()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(selfCopy)
|
||||||
|
log.Printf("running tailscale.exe copy for final install...")
|
||||||
|
|
||||||
|
cmd := exec.Command(selfCopy, "update")
|
||||||
|
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
|
||||||
|
cmd.Stdout = os.Stderr
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Once it's started, exit ourselves, so the binary is free
|
||||||
|
// to be replaced.
|
||||||
|
os.Exit(0)
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func installMSI(msi string) error {
|
||||||
|
var err error
|
||||||
|
for tries := 0; tries < 2; tries++ {
|
||||||
|
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
|
||||||
|
cmd.Dir = filepath.Dir(msi)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
err = cmd.Run()
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
uninstallVersion := version.Short
|
||||||
|
if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
|
||||||
|
uninstallVersion = v
|
||||||
|
}
|
||||||
|
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
|
||||||
|
log.Printf("Uninstalling current version %q for downgrade...", uninstallVersion)
|
||||||
|
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
err = cmd.Run()
|
||||||
|
log.Printf("msiexec uninstall: %v", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func msiUUIDForVersion(ver string) string {
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
if arch == "386" {
|
||||||
|
arch = "x86"
|
||||||
|
}
|
||||||
|
track := "unstable"
|
||||||
|
if stable, ok := versionIsStable(ver); ok && stable {
|
||||||
|
track = "stable"
|
||||||
|
}
|
||||||
|
msiURL := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", track, ver, arch)
|
||||||
|
return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSelfCopy() (tmpPathExe string, err error) {
|
||||||
|
selfExe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
f, err := os.Open(selfExe)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if f := markTempFileFunc; f != nil {
|
||||||
|
if err := f(f2.Name()); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(f2, f); err != nil {
|
||||||
|
f2.Close()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return f2.Name(), f2.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadURLToFile(urlSrc, fileDst string) (ret error) {
|
||||||
|
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||||
|
defer tr.CloseIdleConnections()
|
||||||
|
c := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
quickCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
headReq := must.Get(http.NewRequestWithContext(quickCtx, "HEAD", urlSrc, nil))
|
||||||
|
|
||||||
|
res, err := c.Do(headReq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("HEAD %s: %v", urlSrc, res.Status)
|
||||||
|
}
|
||||||
|
if res.ContentLength <= 0 {
|
||||||
|
return fmt.Errorf("HEAD %s: unexpected Content-Length %v", urlSrc, res.ContentLength)
|
||||||
|
}
|
||||||
|
log.Printf("Download size: %v", res.ContentLength)
|
||||||
|
|
||||||
|
hashReq := must.Get(http.NewRequestWithContext(quickCtx, "GET", urlSrc+".sha256", nil))
|
||||||
|
hashRes, err := c.Do(hashReq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hashHex, err := io.ReadAll(io.LimitReader(hashRes.Body, 100))
|
||||||
|
hashRes.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("GET %s.sha256: %v", urlSrc, res.Status)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wantHash, err := hex.DecodeString(string(strings.TrimSpace(string(hashHex))))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hash := sha256.New()
|
||||||
|
|
||||||
|
dlReq := must.Get(http.NewRequestWithContext(context.Background(), "GET", urlSrc, nil))
|
||||||
|
dlRes, err := c.Do(dlReq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO(bradfitz): resume from existing partial file on disk
|
||||||
|
if dlRes.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("GET %s: %v", urlSrc, dlRes.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
of, err := os.Create(fileDst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if ret != nil {
|
||||||
|
of.Close()
|
||||||
|
// TODO(bradfitz): os.Remove(fileDst) too? or keep it to resume from/debug later.
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
pw := &progressWriter{total: res.ContentLength}
|
||||||
|
n, err := io.Copy(io.MultiWriter(hash, of, pw), io.LimitReader(dlRes.Body, res.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n != res.ContentLength {
|
||||||
|
return fmt.Errorf("downloaded %v; want %v", n, res.ContentLength)
|
||||||
|
}
|
||||||
|
if err := of.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.print()
|
||||||
|
|
||||||
|
if !bytes.Equal(hash.Sum(nil), wantHash) {
|
||||||
|
return fmt.Errorf("SHA-256 of downloaded MSI didn't match expected value")
|
||||||
|
}
|
||||||
|
log.Printf("hash matched")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type progressWriter struct {
|
||||||
|
done int64
|
||||||
|
total int64
|
||||||
|
lastPrint time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *progressWriter) Write(p []byte) (n int, err error) {
|
||||||
|
pw.done += int64(len(p))
|
||||||
|
if time.Since(pw.lastPrint) > 2*time.Second {
|
||||||
|
pw.print()
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *progressWriter) print() {
|
||||||
|
pw.lastPrint = time.Now()
|
||||||
|
log.Printf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
|
||||||
}
|
}
|
||||||
|
21
cmd/tailscale/cli/update_windows.go
Normal file
21
cmd/tailscale/cli/update_windows.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Windows-specific stuff that can't go in update.go because it needs
|
||||||
|
// x/sys/windows.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
markTempFileFunc = markTempFileWindows
|
||||||
|
}
|
||||||
|
|
||||||
|
func markTempFileWindows(name string) error {
|
||||||
|
name16 := windows.StringToUTF16Ptr(name)
|
||||||
|
return windows.MoveFileEx(name16, nil, windows.MOVEFILE_DELAY_UNTIL_REBOOT)
|
||||||
|
}
|
@ -10,7 +10,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||||
D github.com/google/uuid from tailscale.com/util/quarantine
|
github.com/google/uuid from tailscale.com/util/quarantine+
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||||
LW github.com/josharian/native from github.com/mdlayher/netlink+
|
LW github.com/josharian/native from github.com/mdlayher/netlink+
|
||||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||||
@ -49,7 +49,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
💣 tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||||
@ -184,7 +184,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||||
crypto/x509 from crypto/tls+
|
crypto/x509 from crypto/tls+
|
||||||
crypto/x509/pkix from crypto/x509+
|
crypto/x509/pkix from crypto/x509+
|
||||||
D database/sql/driver from github.com/google/uuid
|
database/sql/driver from github.com/google/uuid
|
||||||
embed from tailscale.com/cmd/tailscale/cli+
|
embed from tailscale.com/cmd/tailscale/cli+
|
||||||
encoding from encoding/json+
|
encoding from encoding/json+
|
||||||
encoding/asn1 from crypto/x509+
|
encoding/asn1 from crypto/x509+
|
||||||
|
@ -37,6 +37,9 @@ for file in $(find $1 -name '*.go' -not -path '*/.git/*'); do
|
|||||||
;;
|
;;
|
||||||
$1/wgengine/router/ifconfig_windows.go)
|
$1/wgengine/router/ifconfig_windows.go)
|
||||||
# WireGuard copyright.
|
# WireGuard copyright.
|
||||||
|
;;
|
||||||
|
$1/cmd/tailscale/cli/authenticode_windows.go)
|
||||||
|
# WireGuard copyright.
|
||||||
;;
|
;;
|
||||||
*_string.go)
|
*_string.go)
|
||||||
# Generated file from go:generate stringer
|
# Generated file from go:generate stringer
|
||||||
|
Loading…
Reference in New Issue
Block a user