Compare commits
14 Commits
crawshaw/s
...
crawshaw/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12306507da | ||
|
|
cf2beafbcd | ||
|
|
a7be780155 | ||
|
|
6d1a9017c9 | ||
|
|
a9745a0b68 | ||
|
|
54ba6194f7 | ||
|
|
ecf310be3c | ||
|
|
36a85e1760 | ||
|
|
672b9fd4bd | ||
|
|
0301ccd275 | ||
|
|
e67f1b5da0 | ||
|
|
f01091babe | ||
|
|
4c83bbf850 | ||
|
|
91bc723817 |
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -16,10 +17,14 @@ import (
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
var TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||
var tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
@@ -27,14 +32,16 @@ var tsClient = &http.Client{
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.ConnectDefault()
|
||||
return safesocket.Connect(TailscaledSocket, 41112)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -131,3 +138,67 @@ func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
type WaitingFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
}
|
||||
|
||||
func WaitingFiles(ctx context.Context) ([]WaitingFile, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
var wfs []WaitingFile
|
||||
if err := json.NewDecoder(res.Body).Decode(&wfs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wfs, nil
|
||||
}
|
||||
|
||||
func DeleteWaitingFile(ctx context.Context, baseName string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "DELETE", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
return fmt.Errorf("expected 204 No Content; got HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if res.ContentLength == -1 {
|
||||
res.Body.Close()
|
||||
return nil, 0, fmt.Errorf("unexpected chunking")
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, 0, fmt.Errorf("expected 204 No Content; got HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
return res.Body, res.ContentLength, nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
@@ -88,6 +89,8 @@ change in the future.
|
||||
return err
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
|
||||
@@ -6,12 +6,18 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
@@ -20,12 +26,18 @@ var debugCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
|
||||
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
|
||||
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
goroutines bool
|
||||
ipn bool
|
||||
netMap bool
|
||||
file string
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
@@ -38,6 +50,45 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(goroutines)
|
||||
return nil
|
||||
}
|
||||
if debugArgs.ipn {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if !debugArgs.netMap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
fmt.Printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
if debugArgs.file != "" {
|
||||
if debugArgs.file == "get" {
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(wfs)
|
||||
return nil
|
||||
}
|
||||
delete := strings.HasPrefix(debugArgs.file, "delete:")
|
||||
if delete {
|
||||
return tailscale.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
|
||||
}
|
||||
rc, size, err := tailscale.GetWaitingFile(ctx, debugArgs.file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Size: %v\n", size)
|
||||
io.Copy(os.Stdout, rc)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -24,7 +24,7 @@ require (
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210330185929-1689f2635004
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
|
||||
2
go.sum
2
go.sum
@@ -123,6 +123,8 @@ github.com/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a h1:tQ7Y0ALS
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0 h1:7KFBvUmm3TW/K+bAN22D7M6xSSoY/39s+PajaNBGrLw=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210330185929-1689f2635004 h1:GNEPNdNHsYe5zhoR/0z2Pl/a9zXbr0IySmHV6PhCrzI=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210330185929-1689f2635004/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
|
||||
@@ -68,6 +68,8 @@ type Notify struct {
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
FilesWaiting *empty.Message `json:",omitempty"`
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
// (non-zero) localhost TCP port it's listening on.
|
||||
// This is currently only used by Tailscale when run in the
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -107,6 +108,7 @@ type LocalBackend struct {
|
||||
authURL string
|
||||
interact bool
|
||||
prevIfState *interfaces.State
|
||||
peerAPIServer *peerAPIServer // or nil
|
||||
peerAPIListeners []*peerAPIListener
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
@@ -198,6 +200,14 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
// If the local network configuration has changed, our filter may
|
||||
// need updating to tweak default routes.
|
||||
b.updateFilter(b.netMap, b.prefs)
|
||||
|
||||
if runtime.GOOS == "windows" && b.netMap != nil {
|
||||
want := len(b.netMap.Addresses)
|
||||
b.logf("linkChange: peerAPIListeners too low; trying again")
|
||||
if len(b.peerAPIListeners) < want {
|
||||
go b.initPeerAPIListener()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) onHealthChange(sys health.Subsystem, err error) {
|
||||
@@ -909,15 +919,20 @@ func (b *LocalBackend) readPoller() {
|
||||
// connected, the notification is dropped without being delivered.
|
||||
func (b *LocalBackend) send(n ipn.Notify) {
|
||||
b.mu.Lock()
|
||||
notify := b.notify
|
||||
notifyFunc := b.notify
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
|
||||
if notify != nil {
|
||||
n.Version = version.Long
|
||||
notify(n)
|
||||
} else {
|
||||
if notifyFunc == nil {
|
||||
b.logf("nil notify callback; dropping %+v", n)
|
||||
return
|
||||
}
|
||||
|
||||
n.Version = version.Long
|
||||
if apiSrv != nil && apiSrv.hasFilesWaiting() {
|
||||
n.FilesWaiting = &empty.Message{}
|
||||
}
|
||||
notifyFunc(n)
|
||||
}
|
||||
|
||||
// popBrowserAuthNow shuts down the data plane and sends an auth URL
|
||||
@@ -1489,8 +1504,9 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.peerAPIServer = nil
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
pln.ln.Close()
|
||||
pln.Close()
|
||||
}
|
||||
b.peerAPIListeners = nil
|
||||
|
||||
@@ -1526,21 +1542,34 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
tunName: tunName,
|
||||
selfNode: selfNode,
|
||||
}
|
||||
b.peerAPIServer = ps
|
||||
|
||||
for _, a := range b.netMap.Addresses {
|
||||
ln, err := ps.listen(a.IP, b.prevIfState)
|
||||
if err != nil {
|
||||
b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err)
|
||||
continue
|
||||
isNetstack := wgengine.IsNetstack(b.e)
|
||||
for i, a := range b.netMap.Addresses {
|
||||
var ln net.Listener
|
||||
var err error
|
||||
skipListen := i > 0 && isNetstack
|
||||
if !skipListen {
|
||||
ln, err = ps.listen(a.IP, b.prevIfState)
|
||||
if err != nil {
|
||||
b.logf("[unexpected] peerapi listen(%q) error: %v", a.IP, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
pln := &peerAPIListener{
|
||||
ps: ps,
|
||||
ip: a.IP,
|
||||
ln: ln,
|
||||
ln: ln, // nil for 2nd+ on netstack
|
||||
lb: b,
|
||||
}
|
||||
pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.Port()))
|
||||
|
||||
var port int
|
||||
if skipListen {
|
||||
port = b.peerAPIListeners[0].Port()
|
||||
} else {
|
||||
port = pln.Port()
|
||||
}
|
||||
pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(port))
|
||||
b.logf("peerapi: serving on %s", pln.urlStr)
|
||||
go pln.serve()
|
||||
b.peerAPIListeners = append(b.peerAPIListeners, pln)
|
||||
}
|
||||
@@ -1954,3 +1983,43 @@ func temporarilySetMachineKeyInPersist() bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *LocalBackend) WaitingFiles() ([]WaitingFile, error) {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return nil, errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.WaitingFiles()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) MoveFilesTo(dir string) (filesMoved []string, err error) {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return nil, errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.MoveFilesTo(dir)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DeleteFile(name string) error {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.DeleteFile(name)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err error) {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return nil, 0, errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.OpenFile(name)
|
||||
}
|
||||
|
||||
@@ -22,17 +22,197 @@ import (
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
|
||||
|
||||
type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
rootDir string
|
||||
tunName string
|
||||
selfNode *tailcfg.Node
|
||||
b *LocalBackend
|
||||
rootDir string
|
||||
tunName string
|
||||
selfNode *tailcfg.Node
|
||||
knownEmpty syncs.AtomicBool
|
||||
}
|
||||
|
||||
const partialSuffix = ".tspartial"
|
||||
|
||||
func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
|
||||
clean := path.Clean(baseName)
|
||||
if clean != baseName ||
|
||||
clean == "." ||
|
||||
strings.ContainsAny(clean, `/\`) ||
|
||||
strings.HasSuffix(clean, partialSuffix) {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Join(s.rootDir, strings.ReplaceAll(url.PathEscape(baseName), ":", "%3a")), true
|
||||
}
|
||||
|
||||
// hasFilesWaiting reports whether any files are buffered in the
|
||||
// tailscaled daemon storage.
|
||||
func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
if s.rootDir == "" {
|
||||
return false
|
||||
}
|
||||
if s.knownEmpty.Get() {
|
||||
// Optimization: this is usually empty, so avoid opening
|
||||
// the directory and checking. We can't cache the actual
|
||||
// has-files-or-not values as the macOS/iOS client might
|
||||
// in the future use+delete the files directly. So only
|
||||
// keep this negative cache.
|
||||
return false
|
||||
}
|
||||
f, err := os.Open(s.rootDir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
if strings.HasSuffix(de.Name(), partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
s.knownEmpty.Set(true)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WaitingFile is a JSON-marshaled struct sent by the localapi to pick
|
||||
// up queued files.
|
||||
type WaitingFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) WaitingFiles() (ret []WaitingFile, err error) {
|
||||
if s.rootDir == "" {
|
||||
return nil, errors.New("peerapi disabled; no storage configured")
|
||||
}
|
||||
f, err := os.Open(s.rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
name := de.Name()
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
fi, err := de.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, WaitingFile{
|
||||
Name: filepath.Base(name),
|
||||
Size: fi.Size(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) MoveFilesTo(dir string) (filesMoved []string, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("MoveFilesTo: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if s.rootDir == "" {
|
||||
return nil, errors.New("peerapi disabled; reconsider life choices TODO")
|
||||
}
|
||||
f, err := os.Open(s.rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return filesMoved, err
|
||||
}
|
||||
for _, de := range des {
|
||||
if strings.HasSuffix(de.Name(), partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if !de.Type().IsRegular() {
|
||||
continue
|
||||
}
|
||||
tsdir := filepath.Join(dir, "Tailscale")
|
||||
if err := os.MkdirAll(tsdir, 0777); err != nil {
|
||||
return filesMoved, err
|
||||
}
|
||||
dst := filepath.Join(tsdir, filepath.Base(de.Name()))
|
||||
err = os.Rename(filepath.Join(s.rootDir, de.Name()), dst)
|
||||
if err != nil {
|
||||
return filesMoved, err
|
||||
}
|
||||
filesMoved = append(filesMoved, dst)
|
||||
}
|
||||
}
|
||||
return filesMoved, nil
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
if s.rootDir == "" {
|
||||
return errors.New("peerapi disabled; no storage configured")
|
||||
}
|
||||
path, ok := s.diskPath(baseName)
|
||||
if !ok {
|
||||
return errors.New("bad filename")
|
||||
}
|
||||
err := os.Remove(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
if s.rootDir == "" {
|
||||
return nil, 0, errors.New("peerapi disabled; no storage configured")
|
||||
}
|
||||
path, ok := s.diskPath(baseName)
|
||||
if !ok {
|
||||
return nil, 0, errors.New("bad filename")
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, 0, err
|
||||
}
|
||||
return f, fi.Size(), nil
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) {
|
||||
@@ -51,6 +231,10 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
|
||||
}
|
||||
}
|
||||
|
||||
if wgengine.IsNetstack(s.b.e) {
|
||||
ipStr = ""
|
||||
}
|
||||
|
||||
tcp4or6 := "tcp4"
|
||||
if ip.Is6() {
|
||||
tcp4or6 = "tcp6"
|
||||
@@ -81,12 +265,22 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
|
||||
type peerAPIListener struct {
|
||||
ps *peerAPIServer
|
||||
ip netaddr.IP
|
||||
ln net.Listener
|
||||
ln net.Listener // or nil for 2nd+ address family in netstack mdoe
|
||||
lb *LocalBackend
|
||||
urlStr string
|
||||
}
|
||||
|
||||
func (pln *peerAPIListener) Close() error {
|
||||
if pln.ln != nil {
|
||||
return pln.ln.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pln *peerAPIListener) Port() int {
|
||||
if pln.ln == nil {
|
||||
return 0
|
||||
}
|
||||
ta, ok := pln.ln.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
return 0
|
||||
@@ -95,6 +289,9 @@ func (pln *peerAPIListener) Port() int {
|
||||
}
|
||||
|
||||
func (pln *peerAPIListener) serve() {
|
||||
if pln.ln == nil {
|
||||
return
|
||||
}
|
||||
defer pln.ln.Close()
|
||||
logf := pln.lb.logf
|
||||
for {
|
||||
@@ -202,13 +399,12 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
name := path.Base(r.URL.Path)
|
||||
if name == "." || name == "/" {
|
||||
http.Error(w, "bad filename", http.StatusForbidden)
|
||||
baseName := path.Base(r.URL.Path)
|
||||
dstFile, ok := h.ps.diskPath(baseName)
|
||||
if !ok {
|
||||
http.Error(w, "bad filename", 400)
|
||||
return
|
||||
}
|
||||
fileBase := strings.ReplaceAll(url.PathEscape(name), ":", "%3a")
|
||||
dstFile := filepath.Join(h.ps.rootDir, fileBase)
|
||||
f, err := os.Create(dstFile)
|
||||
if err != nil {
|
||||
h.logf("put Create error: %v", err)
|
||||
@@ -234,10 +430,22 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.logf("put(%q): %d bytes from %v/%v", name, n, h.remoteAddr.IP, h.peerNode.ComputedName)
|
||||
h.logf("put of %s from %v/%v", baseName, approxSize(n), h.remoteAddr.IP, h.peerNode.ComputedName)
|
||||
|
||||
// TODO: set modtime
|
||||
// TODO: some real response
|
||||
success = true
|
||||
io.WriteString(w, "{}\n")
|
||||
h.ps.knownEmpty.Set(false)
|
||||
h.ps.b.send(ipn.Notify{}) // it will set FilesWaiting
|
||||
}
|
||||
|
||||
func approxSize(n int64) string {
|
||||
if n <= 1<<10 {
|
||||
return "<=1KB"
|
||||
}
|
||||
if n <= 1<<20 {
|
||||
return "<=1MB"
|
||||
}
|
||||
return fmt.Sprintf("~%dMB", n/1<<20)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
@@ -53,6 +56,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/v0/files/") {
|
||||
h.serveFiles(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
h.serveWhoIs(w, r)
|
||||
@@ -131,6 +138,49 @@ func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
|
||||
e.Encode(st)
|
||||
}
|
||||
|
||||
func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "file access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
suffix := strings.TrimPrefix(r.URL.Path, "/localapi/v0/files/")
|
||||
if suffix == "" {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET to list files", 400)
|
||||
return
|
||||
}
|
||||
wfs, err := h.b.WaitingFiles()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(wfs)
|
||||
return
|
||||
}
|
||||
name, err := url.PathUnescape(suffix)
|
||||
if err != nil {
|
||||
http.Error(w, "bad filename", 400)
|
||||
return
|
||||
}
|
||||
if r.Method == "DELETE" {
|
||||
if err := h.b.DeleteFile(name); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
rc, size, err := h.b.OpenFile(name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
w.Header().Set("Content-Length", fmt.Sprint(size))
|
||||
io.Copy(w, rc)
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
|
||||
@@ -500,7 +500,8 @@ func isPrivateIP(ip netaddr.IP) bool {
|
||||
}
|
||||
|
||||
func isGlobalV6(ip netaddr.IP) bool {
|
||||
return v6Global1.Contains(ip)
|
||||
return v6Global1.Contains(ip) ||
|
||||
(tsaddr.IsULA(ip) && !tsaddr.TailscaleULARange().Contains(ip))
|
||||
}
|
||||
|
||||
func mustCIDR(s string) netaddr.IPPrefix {
|
||||
|
||||
@@ -2,16 +2,85 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux darwin,!redo
|
||||
// +build linux,!redo
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultRouteInterface(t *testing.T) {
|
||||
// tests /proc/net/route on the local system, cannot make an assertion about
|
||||
// the correct interface name, but good as a sanity check.
|
||||
v, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got %q", v)
|
||||
}
|
||||
|
||||
// test the specific /proc/net/route path as found on Google Cloud Run instances
|
||||
func TestGoogleCloudRunDefaultRouteInterface(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
savedProcNetRoutePath := procNetRoutePath
|
||||
defer func() { procNetRoutePath = savedProcNetRoutePath }()
|
||||
procNetRoutePath = filepath.Join(dir, "CloudRun")
|
||||
buf := []byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
|
||||
"eth0\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n" +
|
||||
"eth1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n")
|
||||
err := ioutil.WriteFile(procNetRoutePath, buf, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "eth1" {
|
||||
t.Fatalf("got %s, want eth1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// we read chunks of /proc/net/route at a time, test that files longer than the chunk
|
||||
// size can be handled.
|
||||
func TestExtremelyLongProcNetRoute(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
savedProcNetRoutePath := procNetRoutePath
|
||||
defer func() { procNetRoutePath = savedProcNetRoutePath }()
|
||||
procNetRoutePath = filepath.Join(dir, "VeryLong")
|
||||
f, err := os.Create(procNetRoutePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.Write([]byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for n := 0; n <= 1000; n++ {
|
||||
line := fmt.Sprintf("eth%d\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n", n)
|
||||
_, err := f.Write([]byte(line))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
_, err = f.Write([]byte("tokenring1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != "tokenring1" {
|
||||
t.Fatalf("got %q, want tokenring1", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,14 +135,16 @@ func DefaultRouteInterface() (string, error) {
|
||||
}
|
||||
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
var procNetRoutePath = "/proc/net/route"
|
||||
|
||||
func defaultRouteInterfaceProcNet() (string, error) {
|
||||
f, err := os.Open("/proc/net/route")
|
||||
func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
|
||||
f, err := os.Open(procNetRoutePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
br := bufio.NewReaderSize(f, 128)
|
||||
|
||||
br := bufio.NewReaderSize(f, bufsize)
|
||||
for {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
@@ -170,7 +172,25 @@ func defaultRouteInterfaceProcNet() (string, error) {
|
||||
}
|
||||
|
||||
return "", errors.New("no default routes found")
|
||||
}
|
||||
|
||||
func defaultRouteInterfaceProcNet() (string, error) {
|
||||
rc, err := defaultRouteInterfaceProcNetInternal(128)
|
||||
if rc == "" && (err == io.EOF || err == nil) {
|
||||
// https://github.com/google/gvisor/issues/5732
|
||||
// On a regular Linux kernel you can read the first 128 bytes of /proc/net/route,
|
||||
// then come back later to read the next 128 bytes and so on.
|
||||
//
|
||||
// In Google Cloud Run, where /proc/net/route comes from gVisor, you have to
|
||||
// read it all at once. If you read only the first few bytes then the second
|
||||
// read returns 0 bytes no matter how much originally appeared to be in the file.
|
||||
//
|
||||
// At the time of this writing (Mar 2021) Google Cloud Run has eth0 and eth1
|
||||
// with a 384 byte /proc/net/route. We allocate a large buffer to ensure we'll
|
||||
// read it all in one call.
|
||||
return defaultRouteInterfaceProcNetInternal(4096)
|
||||
}
|
||||
return rc, err
|
||||
}
|
||||
|
||||
// defaultRouteInterfaceAndroidIPRoute tries to find the machine's default route interface name
|
||||
|
||||
@@ -7,6 +7,8 @@ package interfaces
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestGetState(t *testing.T) {
|
||||
@@ -43,3 +45,24 @@ func TestLikelyHomeRouterIP(t *testing.T) {
|
||||
}
|
||||
t.Logf("myIP = %v; gw = %v", my, gw)
|
||||
}
|
||||
|
||||
func TestIsGlobalV6(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"first ULA", "fc00::1", true},
|
||||
{"Tailscale", "fd7a:115c:a1e0::1", false},
|
||||
{"Cloud Run", "fddf:3978:feb1:d745::1", true},
|
||||
{"zeros", "0000:0000:0000:0000:0000:0000:0000:0000", false},
|
||||
{"Link Local", "fe80::1", false},
|
||||
{"Global", "2602::1", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := isGlobalV6(netaddr.MustParseIP(test.ip)); got != test.want {
|
||||
t.Errorf("isGlobalV6(%s) = %v, want %v", test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func CGNATRange() netaddr.IPPrefix {
|
||||
var (
|
||||
cgnatRange oncePrefix
|
||||
ulaRange oncePrefix
|
||||
tsUlaRange oncePrefix
|
||||
ula4To6Range oncePrefix
|
||||
)
|
||||
|
||||
@@ -57,8 +58,8 @@ func IsTailscaleIP(ip netaddr.IP) bool {
|
||||
// TailscaleULARange returns the IPv6 Unique Local Address range that
|
||||
// is the superset range that Tailscale assigns out of.
|
||||
func TailscaleULARange() netaddr.IPPrefix {
|
||||
ulaRange.Do(func() { mustPrefix(&ulaRange.v, "fd7a:115c:a1e0::/48") })
|
||||
return ulaRange.v
|
||||
tsUlaRange.Do(func() { mustPrefix(&tsUlaRange.v, "fd7a:115c:a1e0::/48") })
|
||||
return tsUlaRange.v
|
||||
}
|
||||
|
||||
// Tailscale4To6Range returns the subset of TailscaleULARange used for
|
||||
@@ -95,6 +96,11 @@ func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
|
||||
return netaddr.IPFrom16(ret)
|
||||
}
|
||||
|
||||
func IsULA(ip netaddr.IP) bool {
|
||||
ulaRange.Do(func() { mustPrefix(&ulaRange.v, "fc00::/7") })
|
||||
return ulaRange.v.Contains(ip)
|
||||
}
|
||||
|
||||
func mustPrefix(v *netaddr.IPPrefix, prefix string) {
|
||||
var err error
|
||||
*v, err = netaddr.ParseIPPrefix(prefix)
|
||||
|
||||
@@ -42,3 +42,25 @@ func TestCGNATRange(t *testing.T) {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUla(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"first ULA", "fc00::1", true},
|
||||
{"not ULA", "fb00::1", false},
|
||||
{"Tailscale", "fd7a:115c:a1e0::1", true},
|
||||
{"Cloud Run", "fddf:3978:feb1:d745::1", true},
|
||||
{"zeros", "0000:0000:0000:0000:0000:0000:0000:0000", false},
|
||||
{"Link Local", "fe80::1", false},
|
||||
{"Global", "2602::1", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := IsULA(netaddr.MustParseIP(test.ip)); got != test.want {
|
||||
t.Errorf("IsULA(%s) = %v, want %v", test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/paths"
|
||||
)
|
||||
|
||||
type closeable interface {
|
||||
@@ -31,11 +29,6 @@ func ConnCloseWrite(c net.Conn) error {
|
||||
return c.(closeable).CloseWrite()
|
||||
}
|
||||
|
||||
// ConnectDefault connects to the local Tailscale daemon.
|
||||
func ConnectDefault() (net.Conn, error) {
|
||||
return Connect(paths.DefaultTailscaledSocket(), 41112)
|
||||
}
|
||||
|
||||
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
|
||||
func Connect(path string, port uint16) (net.Conn, error) {
|
||||
return connect(path, port)
|
||||
|
||||
@@ -228,15 +228,14 @@ func (s *magicStack) Status() *ipnstate.Status {
|
||||
// Something external needs to provide a NetworkMap and WireGuard
|
||||
// configs to the magicStack in order for it to acquire an IP
|
||||
// address. See meshStacks for one possible source of netmaps and IPs.
|
||||
func (s *magicStack) IP(t *testing.T) netaddr.IP {
|
||||
func (s *magicStack) IP() netaddr.IP {
|
||||
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
|
||||
st := s.Status()
|
||||
if len(st.TailscaleIPs) > 0 {
|
||||
return st.TailscaleIPs[0]
|
||||
}
|
||||
}
|
||||
t.Fatal("timed out waiting for magicstack to get an IP assigned")
|
||||
panic("unreachable") // compiler doesn't know t.Fatal panics
|
||||
panic("timed out waiting for magicstack to get an IP assigned")
|
||||
}
|
||||
|
||||
// meshStacks monitors epCh on all given ms, and plumbs network maps
|
||||
@@ -566,7 +565,7 @@ func TestConnClosed(t *testing.T) {
|
||||
cleanup = meshStacks(t.Logf, []*magicStack{ms1, ms2})
|
||||
defer cleanup()
|
||||
|
||||
pkt := tuntest.Ping(ms2.IP(t).IPAddr().IP, ms1.IP(t).IPAddr().IP)
|
||||
pkt := tuntest.Ping(ms2.IP().IPAddr().IP, ms1.IP().IPAddr().IP)
|
||||
|
||||
if len(ms1.conn.activeDerp) == 0 {
|
||||
t.Errorf("unexpected DERP empty got: %v want: >0", len(ms1.conn.activeDerp))
|
||||
@@ -767,7 +766,7 @@ func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup fu
|
||||
// failure). Figure out what kind of thing would be
|
||||
// acceptable to test instead of "every ping must
|
||||
// transit".
|
||||
pkt := tuntest.Ping(dst.IP(t).IPAddr().IP, src.IP(t).IPAddr().IP)
|
||||
pkt := tuntest.Ping(dst.IP().IPAddr().IP, src.IP().IPAddr().IP)
|
||||
select {
|
||||
case src.tun.Outbound <- pkt:
|
||||
case <-ctx.Done():
|
||||
@@ -812,7 +811,7 @@ func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup fu
|
||||
}
|
||||
|
||||
go func() {
|
||||
logf("sending ping stream from %s (%s) to %s (%s)", src, src.IP(t), dst, dst.IP(t))
|
||||
logf("sending ping stream from %s (%s) to %s (%s)", src, src.IP(), dst, dst.IP())
|
||||
defer close(done)
|
||||
for one() {
|
||||
}
|
||||
@@ -852,8 +851,8 @@ func testActiveDiscovery(t *testing.T, d *devices) {
|
||||
cleanup = meshStacks(logf, []*magicStack{m1, m2})
|
||||
defer cleanup()
|
||||
|
||||
m1IP := m1.IP(t)
|
||||
m2IP := m2.IP(t)
|
||||
m1IP := m1.IP()
|
||||
m2IP := m2.IP()
|
||||
logf("IPs: %s %s", m1IP, m2IP)
|
||||
|
||||
cleanup = newPinger(t, logf, m1, m2)
|
||||
|
||||
@@ -164,6 +164,20 @@ func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error)
|
||||
})
|
||||
}
|
||||
|
||||
// IsNetstack reports whether e is a netstack-based TUN-free engine.
|
||||
func IsNetstack(e Engine) bool {
|
||||
ig, ok := e.(InternalsGetter)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
tw, _, ok := ig.GetInternals()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
name, err := tw.Name()
|
||||
return err == nil && name == "FakeTUN"
|
||||
}
|
||||
|
||||
// NewUserspaceEngine creates the named tun device and returns a
|
||||
// Tailscale Engine running on it.
|
||||
func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) {
|
||||
@@ -1438,11 +1452,30 @@ func (e *userspaceEngine) UnregisterIPPortIdentity(ipport netaddr.IPPort) {
|
||||
delete(e.tsIPByIPPort, ipport)
|
||||
}
|
||||
|
||||
var whoIsSleeps = [...]time.Duration{
|
||||
0,
|
||||
10 * time.Millisecond,
|
||||
20 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
100 * time.Millisecond,
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) WhoIsIPPort(ipport netaddr.IPPort) (tsIP netaddr.IP, ok bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
tsIP, ok = e.tsIPByIPPort[ipport]
|
||||
return tsIP, ok
|
||||
// We currently have a registration race,
|
||||
// https://github.com/tailscale/tailscale/issues/1616,
|
||||
// so loop a few times for now waiting for the registration
|
||||
// to appear.
|
||||
// TODO(bradfitz,namansood): remove this once #1616 is fixed.
|
||||
for _, d := range whoIsSleeps {
|
||||
time.Sleep(d)
|
||||
e.mu.Lock()
|
||||
tsIP, ok = e.tsIPByIPPort[ipport]
|
||||
e.mu.Unlock()
|
||||
if ok {
|
||||
return tsIP, true
|
||||
}
|
||||
}
|
||||
return tsIP, false
|
||||
}
|
||||
|
||||
// peerForIP returns the Node in the wireguard config
|
||||
|
||||
@@ -186,3 +186,14 @@ func BenchmarkGenLocalAddrFunc(b *testing.B) {
|
||||
})
|
||||
b.Logf("x = %v", x)
|
||||
}
|
||||
|
||||
func TestIsNetstack(t *testing.T) {
|
||||
e, err := NewUserspaceEngine(t.Logf, Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer e.Close()
|
||||
if !IsNetstack(e) {
|
||||
t.Errorf("IsNetstack = false; want true")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user