Compare commits

...

14 Commits

Author SHA1 Message Date
David Crawshaw
12306507da ipnlocal: add MoveFilesTo method
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-03-31 14:22:55 -07:00
Brad Fitzpatrick
cf2beafbcd ipn/ipnlocal: on Windows peerapi bind failures, try again on link change
Updates #1620

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-03-30 13:49:37 -07:00
Brad Fitzpatrick
a7be780155 go.mod, go.sum: bump wireguard-go 2021-03-30 13:05:23 -07:00
Brad Fitzpatrick
6d1a9017c9 ipn/{ipnlocal,localapi}, client/tailscale: add file get/delete APIs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-03-30 12:56:51 -07:00
Denton Gentry
a9745a0b68 interfaces: try larger read from /proc/net/route
Work around https://github.com/google/gvisor/issues/5732
by trying to read /proc/net/route with a larger bufsize if
it fails the first time.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-03-30 12:33:53 -07:00
Denton Gentry
54ba6194f7 interfaces: allow IPv6 ULA as a valid address.
IPv6 Unique Local Addresses are sometimes used with Network
Prefix Translation to reach the Internet. In that respect
their use is similar to the private IPv4 address ranges
10/8, 172.16/12, and 192.168/16.

Treat them as sufficient for AnyInterfaceUp(), but specifically
exclude Tailscale's own IPv6 ULA prefix to avoid mistakenly
trying to bootstrap Tailscale using Tailscale.

This helps in supporting Google Cloud Run, where the addresses
are 169.254.8.1/32 and fddf:3978:feb1:d745::c001/128 on eth1.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-03-30 12:33:53 -07:00
Denton Gentry
ecf310be3c net/tsaddr: IsUla() for IPv6 Unique Local Address
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-03-30 12:33:53 -07:00
Josh Bleecher Snyder
36a85e1760 wgengine/magicsock: don't call t.Fatal in magicStack.IP
It can end up executing an a new goroutine,
at which point instead of immediately stopping test execution, it hangs.
Since this is unexpected anyway, panic instead.
As a bonus, it makes call sites nicer and removes a kludge comment.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-03-30 11:48:13 -07:00
Brad Fitzpatrick
672b9fd4bd ipn{,/ipnlocal}: set new Notify.FilesWaiting when server has file(s)
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-03-30 11:36:12 -07:00
Brad Fitzpatrick
0301ccd275 cmd/tailscale/cli: add debug --ipn mode
To watch the IPN message bus.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-03-30 10:43:36 -07:00
David Crawshaw
e67f1b5da0 client/tailscale, cmd/tailscale/cli: plumb --socket through
Without this, `tailscale status` ignores the --socket flag on macOS and
always talks to the IPNExtension, even if you wanted it to inspect a
userspace tailscaled.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-03-30 10:09:14 -07:00
Brad Fitzpatrick
f01091babe ipn/ipnlocal: make peerapi work in netstack mode
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-03-30 09:55:01 -07:00
Brad Fitzpatrick
4c83bbf850 wgengine: add IsNetstack func and test
So we have a documented & tested way to check whether we're in
netstack mode. To be used by future commits.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-03-30 09:53:12 -07:00
Brad Fitzpatrick
91bc723817 wgengine: add temp workaround for netstack WhoIs registration race
Updates #1616

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-03-30 09:50:54 -07:00
19 changed files with 693 additions and 60 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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")
}
}