summaryrefslogtreecommitdiffhomepage
path: root/ipn
diff options
context:
space:
mode:
Diffstat (limited to 'ipn')
-rw-r--r--ipn/backend.go11
-rw-r--r--ipn/fake_test.go10
-rw-r--r--ipn/handle.go8
-rw-r--r--ipn/ipnlocal/local.go202
-rw-r--r--ipn/ipnlocal/local_test.go32
-rw-r--r--ipn/ipnlocal/peerapi.go167
-rw-r--r--ipn/ipnlocal/peerapi_macios_ext.go54
-rw-r--r--ipn/ipnstate/ipnstate.go59
-rw-r--r--ipn/localapi/localapi.go32
-rw-r--r--ipn/message.go21
-rw-r--r--ipn/message_test.go4
11 files changed, 486 insertions, 114 deletions
diff --git a/ipn/backend.go b/ipn/backend.go
index 9352853b1..dee548c54 100644
--- a/ipn/backend.go
+++ b/ipn/backend.go
@@ -8,7 +8,6 @@ import (
"net/http"
"time"
- "golang.org/x/oauth2"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
@@ -28,7 +27,7 @@ const (
Running
)
-// GoogleIDToken Type is the oauth2.Token.TokenType for the Google
+// GoogleIDToken Type is the tailcfg.Oauth2Token.TokenType for the Google
// ID tokens used by the Android client.
const GoogleIDTokenType = "ts_android_google_login"
@@ -65,7 +64,6 @@ type Notify struct {
Prefs *Prefs // preferences were changed
NetMap *netmap.NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
- Status *ipnstate.Status // full status
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
PingResult *ipnstate.PingResult
@@ -143,7 +141,7 @@ type Backend interface {
// eventually.
StartLoginInteractive()
// Login logs in with an OAuth2 token.
- Login(token *oauth2.Token)
+ Login(token *tailcfg.Oauth2Token)
// Logout terminates the current login session and stops the
// wireguard engine.
Logout()
@@ -159,9 +157,6 @@ type Backend interface {
// counts. Connection events are emitted automatically without
// polling.
RequestEngineStatus()
- // RequestStatus requests that a full Status update
- // notification is sent.
- RequestStatus()
// FakeExpireAfter pretends that the current key is going to
// expire after duration x. This is useful for testing GUIs to
// make sure they react properly with keys that are going to
@@ -170,5 +165,5 @@ type Backend interface {
// Ping attempts to start connecting to the given IP and sends a Notify
// with its PingResult. If the host is down, there might never
// be a PingResult sent. The cmd/tailscale CLI client adds a timeout.
- Ping(ip string)
+ Ping(ip string, useTSMP bool)
}
diff --git a/ipn/fake_test.go b/ipn/fake_test.go
index e918f77f0..eef580f57 100644
--- a/ipn/fake_test.go
+++ b/ipn/fake_test.go
@@ -8,8 +8,8 @@ import (
"log"
"time"
- "golang.org/x/oauth2"
"tailscale.com/ipn/ipnstate"
+ "tailscale.com/tailcfg"
"tailscale.com/types/netmap"
)
@@ -46,7 +46,7 @@ func (b *FakeBackend) StartLoginInteractive() {
b.login()
}
-func (b *FakeBackend) Login(token *oauth2.Token) {
+func (b *FakeBackend) Login(token *tailcfg.Oauth2Token) {
b.login()
}
@@ -87,14 +87,10 @@ func (b *FakeBackend) RequestEngineStatus() {
b.notify(Notify{Engine: &EngineStatus{}})
}
-func (b *FakeBackend) RequestStatus() {
- b.notify(Notify{Status: &ipnstate.Status{}})
-}
-
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
}
-func (b *FakeBackend) Ping(ip string) {
+func (b *FakeBackend) Ping(ip string, useTSMP bool) {
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
}
diff --git a/ipn/handle.go b/ipn/handle.go
index 91b757f56..54a61140e 100644
--- a/ipn/handle.go
+++ b/ipn/handle.go
@@ -8,8 +8,8 @@ import (
"sync"
"time"
- "golang.org/x/oauth2"
"inet.af/netaddr"
+ "tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
)
@@ -155,7 +155,7 @@ func (h *Handle) StartLoginInteractive() {
h.b.StartLoginInteractive()
}
-func (h *Handle) Login(token *oauth2.Token) {
+func (h *Handle) Login(token *tailcfg.Oauth2Token) {
h.b.Login(token)
}
@@ -167,10 +167,6 @@ func (h *Handle) RequestEngineStatus() {
h.b.RequestEngineStatus()
}
-func (h *Handle) RequestStatus() {
- h.b.RequestStatus()
-}
-
func (h *Handle) FakeExpireAfter(x time.Duration) {
h.b.FakeExpireAfter(x)
}
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index 38c9323d7..f6173a763 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -9,13 +9,14 @@ import (
"context"
"errors"
"fmt"
+ "net"
"os"
"runtime"
+ "strconv"
"strings"
"sync"
"time"
- "golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/health"
@@ -23,6 +24,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
+ "tailscale.com/net/dns"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/portlist"
@@ -38,8 +40,6 @@ import (
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router"
- "tailscale.com/wgengine/router/dns"
- "tailscale.com/wgengine/tsdns"
"tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgcfg/nmcfg"
)
@@ -96,15 +96,16 @@ type LocalBackend struct {
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
- netMap *netmap.NetworkMap
- nodeByAddr map[netaddr.IP]*tailcfg.Node
- activeLogin string // last logged LoginName from netMap
- engineStatus ipn.EngineStatus
- endpoints []string
- blocked bool
- authURL string
- interact bool
- prevIfState *interfaces.State
+ netMap *netmap.NetworkMap
+ nodeByAddr map[netaddr.IP]*tailcfg.Node
+ activeLogin string // last logged LoginName from netMap
+ engineStatus ipn.EngineStatus
+ endpoints []string
+ blocked bool
+ authURL string
+ interact bool
+ prevIfState *interfaces.State
+ peerAPIListeners []*peerAPIListener
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
@@ -144,6 +145,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
b.statusChanged = sync.NewCond(&b.statusLock)
linkMon := e.GetLinkMonitor()
+ b.prevIfState = linkMon.InterfaceState()
// Call our linkChange code once with the current state, and
// then also whenever it changes:
b.linkChange(false, linkMon.InterfaceState())
@@ -218,52 +220,82 @@ func (b *LocalBackend) Status() *ipnstate.Status {
return sb.Status()
}
+// StatusWithoutPeers is like Status but omits any details
+// of peers.
+func (b *LocalBackend) StatusWithoutPeers() *ipnstate.Status {
+ sb := new(ipnstate.StatusBuilder)
+ b.updateStatus(sb, nil)
+ return sb.Status()
+}
+
// UpdateStatus implements ipnstate.StatusUpdater.
func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
b.e.UpdateStatus(sb)
+ b.updateStatus(sb, b.populatePeerStatusLocked)
+}
+// updateStatus populates sb with status.
+//
+// extraLocked, if non-nil, is called while b.mu is still held.
+func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) {
b.mu.Lock()
defer b.mu.Unlock()
-
- sb.SetBackendState(b.state.String())
- sb.SetAuthURL(b.authURL)
-
+ sb.MutateStatus(func(s *ipnstate.Status) {
+ s.Version = version.Long
+ s.BackendState = b.state.String()
+ s.AuthURL = b.authURL
+ if b.netMap != nil {
+ s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
+ }
+ })
+ sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
+ for _, pln := range b.peerAPIListeners {
+ ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
+ }
+ })
// TODO: hostinfo, and its networkinfo
// TODO: EngineStatus copy (and deprecate it?)
- if b.netMap != nil {
- sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix())
- for id, up := range b.netMap.UserProfiles {
- sb.AddUser(id, up)
+
+ if extraLocked != nil {
+ extraLocked(sb)
+ }
+}
+
+func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
+ if b.netMap == nil {
+ return
+ }
+ for id, up := range b.netMap.UserProfiles {
+ sb.AddUser(id, up)
+ }
+ for _, p := range b.netMap.Peers {
+ var lastSeen time.Time
+ if p.LastSeen != nil {
+ lastSeen = *p.LastSeen
}
- for _, p := range b.netMap.Peers {
- var lastSeen time.Time
- if p.LastSeen != nil {
- lastSeen = *p.LastSeen
- }
- var tailAddr string
- for _, addr := range p.Addresses {
- // The peer struct currently only allows a single
- // Tailscale IP address. For compatibility with the
- // old display, make sure it's the IPv4 address.
- if addr.IP.Is4() && addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) {
- tailAddr = addr.IP.String()
- break
- }
+ var tailAddr string
+ for _, addr := range p.Addresses {
+ // The peer struct currently only allows a single
+ // Tailscale IP address. For compatibility with the
+ // old display, make sure it's the IPv4 address.
+ if addr.IP.Is4() && addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) {
+ tailAddr = addr.IP.String()
+ break
}
- sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
- InNetworkMap: true,
- UserID: p.User,
- TailAddr: tailAddr,
- HostName: p.Hostinfo.Hostname,
- DNSName: p.Name,
- OS: p.Hostinfo.OS,
- KeepAlive: p.KeepAlive,
- Created: p.Created,
- LastSeen: lastSeen,
- ShareeNode: p.Hostinfo.ShareeNode,
- ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
- })
}
+ sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
+ InNetworkMap: true,
+ UserID: p.User,
+ TailAddr: tailAddr,
+ HostName: p.Hostinfo.Hostname,
+ DNSName: p.Name,
+ OS: p.Hostinfo.OS,
+ KeepAlive: p.KeepAlive,
+ Created: p.Created,
+ LastSeen: lastSeen,
+ ShareeNode: p.Hostinfo.ShareeNode,
+ ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
+ })
}
}
@@ -697,10 +729,16 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("192.168.0.0/16"),
netaddr.MustParseIPPrefix("172.16.0.0/12"),
netaddr.MustParseIPPrefix("10.0.0.0/8"),
+ // IPv4 link-local
+ netaddr.MustParseIPPrefix("169.254.0.0/16"),
+ // IPv4 multicast
+ netaddr.MustParseIPPrefix("224.0.0.0/4"),
// Tailscale IPv4 range
tsaddr.CGNATRange(),
// IPv6 Link-local addresses
netaddr.MustParseIPPrefix("fe80::/10"),
+ // IPv6 multicast
+ netaddr.MustParseIPPrefix("ff00::/8"),
// Tailscale IPv6 range
tsaddr.TailscaleULARange(),
}
@@ -710,6 +748,7 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{
func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
var b netaddr.IPSetBuilder
b.AddPrefix(route)
+ var hostIPs []netaddr.IP
err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) {
if tsaddr.IsTailscaleIP(pfx.IP) {
return
@@ -717,11 +756,25 @@ func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
if pfx.IsSingleIP() {
return
}
+ hostIPs = append(hostIPs, pfx.IP)
b.RemovePrefix(pfx)
})
if err != nil {
return nil, err
}
+
+ // Having removed all the LAN subnets, re-add the hosts's own
+ // IPs. It's fine for clients to connect to an exit node's public
+ // IP address, just not the attached subnet.
+ //
+ // Truly forbidden subnets (in removeFromDefaultRoute) will still
+ // be stripped back out by the next step.
+ for _, ip := range hostIPs {
+ if route.Contains(ip) {
+ b.Add(ip)
+ }
+ }
+
for _, pfx := range removeFromDefaultRoute {
b.RemovePrefix(pfx)
}
@@ -796,8 +849,8 @@ func (b *LocalBackend) updateDNSMap(netMap *netmap.NetworkMap) {
}
set(netMap.Name, netMap.Addresses)
- dnsMap := tsdns.NewMap(nameToIP, magicDNSRootDomains(netMap))
- // map diff will be logged in tsdns.Resolver.SetMap.
+ dnsMap := dns.NewMap(nameToIP, magicDNSRootDomains(netMap))
+ // map diff will be logged in dns.Resolver.SetMap.
b.e.SetDNSMap(dnsMap)
}
@@ -1078,7 +1131,7 @@ func (b *LocalBackend) getEngineStatus() ipn.EngineStatus {
}
// Login implements Backend.
-func (b *LocalBackend) Login(token *oauth2.Token) {
+func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) {
b.mu.Lock()
b.assertClientLocked()
c := b.c
@@ -1129,13 +1182,13 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
b.send(ipn.Notify{NetMap: b.netMap})
}
-func (b *LocalBackend) Ping(ipStr string) {
+func (b *LocalBackend) Ping(ipStr string, useTSMP bool) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
b.logf("ignoring Ping request to invalid IP %q", ipStr)
return
}
- b.e.Ping(ip, func(pr *ipnstate.PingResult) {
+ b.e.Ping(ip, useTSMP, func(pr *ipnstate.PingResult) {
b.send(ipn.Notify{PingResult: pr})
})
}
@@ -1382,6 +1435,45 @@ func (b *LocalBackend) authReconfig() {
return
}
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
+
+ b.initPeerAPIListener()
+}
+
+func (b *LocalBackend) initPeerAPIListener() {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ for _, pln := range b.peerAPIListeners {
+ pln.ln.Close()
+ }
+ b.peerAPIListeners = nil
+
+ if len(b.netMap.Addresses) == 0 || b.netMap.SelfNode == nil {
+ return
+ }
+
+ var tunName string
+ if ge, ok := b.e.(wgengine.InternalsGetter); ok {
+ tunDev, _ := ge.GetInternals()
+ tunName, _ = tunDev.Name()
+ }
+
+ for _, a := range b.netMap.Addresses {
+ ln, err := peerAPIListen(a.IP, b.prevIfState, tunName)
+ if err != nil {
+ b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err)
+ continue
+ }
+ pln := &peerAPIListener{
+ ln: ln,
+ lb: b,
+ selfNode: b.netMap.SelfNode,
+ }
+ pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.Port()))
+
+ go pln.serve()
+ b.peerAPIListeners = append(b.peerAPIListeners, pln)
+ }
}
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
@@ -1617,12 +1709,6 @@ func (b *LocalBackend) RequestEngineStatus() {
b.e.RequestStatus()
}
-// RequestStatus implements Backend.
-func (b *LocalBackend) RequestStatus() {
- st := b.Status()
- b.send(ipn.Notify{Status: st})
-}
-
// stateMachine updates the state machine state based on other things
// that have happened. It is invoked from the various callbacks that
// feed events into LocalBackend.
diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go
index 667cc2287..2a6fc9315 100644
--- a/ipn/ipnlocal/local_test.go
+++ b/ipn/ipnlocal/local_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"inet.af/netaddr"
+ "tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/netmap"
@@ -122,11 +123,21 @@ func TestNetworkMapCompare(t *testing.T) {
}
}
+func inRemove(ip netaddr.IP) bool {
+ for _, pfx := range removeFromDefaultRoute {
+ if pfx.Contains(ip) {
+ return true
+ }
+ }
+ return false
+}
+
func TestShrinkDefaultRoute(t *testing.T) {
tests := []struct {
- route string
- in []string
- out []string
+ route string
+ in []string
+ out []string
+ localIPFn func(netaddr.IP) bool // true if this machine's local IP address should be "in" after shrinking.
}{
{
route: "0.0.0.0/0",
@@ -139,19 +150,24 @@ func TestShrinkDefaultRoute(t *testing.T) {
"172.16.0.1",
"172.31.255.255",
"100.101.102.103",
+ "224.0.0.1",
+ "169.254.169.254",
// Some random IPv6 stuff that shouldn't be in a v4
// default route.
"fe80::",
"2601::1",
},
+ localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is4() },
},
{
route: "::/0",
in: []string{"::1", "2601::1"},
out: []string{
"fe80::1",
+ "ff00::1",
tsaddr.TailscaleULARange().IP.String(),
},
+ localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is6() },
},
}
@@ -171,6 +187,16 @@ func TestShrinkDefaultRoute(t *testing.T) {
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
}
}
+ ips, _, err := interfaces.LocalAddresses()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, ip := range ips {
+ want := test.localIPFn(ip)
+ if gotContains := got.Contains(ip); gotContains != want {
+ t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want)
+ }
+ }
}
}
diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go
new file mode 100644
index 000000000..390b45efc
--- /dev/null
+++ b/ipn/ipnlocal/peerapi.go
@@ -0,0 +1,167 @@
+// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ipnlocal
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "hash/crc32"
+ "html"
+ "io"
+ "net"
+ "net/http"
+ "runtime"
+ "strconv"
+
+ "inet.af/netaddr"
+ "tailscale.com/net/interfaces"
+ "tailscale.com/tailcfg"
+)
+
+var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
+
+func peerAPIListen(ip netaddr.IP, ifState *interfaces.State, tunIfName string) (ln net.Listener, err error) {
+ ipStr := ip.String()
+
+ var lc net.ListenConfig
+ if initListenConfig != nil {
+ // On iOS/macOS, this sets the lc.Control hook to
+ // setsockopt the interface index to bind to, to get
+ // out of the network sandbox.
+ if err := initListenConfig(&lc, ip, ifState, tunIfName); err != nil {
+ return nil, err
+ }
+ if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
+ ipStr = ""
+ }
+ }
+
+ tcp4or6 := "tcp4"
+ if ip.Is6() {
+ tcp4or6 = "tcp6"
+ }
+
+ // Make a best effort to pick a deterministic port number for
+ // the ip The lower three bytes are the same for IPv4 and IPv6
+ // Tailscale addresses (at least currently), so we'll usually
+ // get the same port number on both address families for
+ // dev/debugging purposes, which is nice. But it's not so
+ // deterministic that people will bake this into clients.
+ // We try a few times just in case something's already
+ // listening on that port (on all interfaces, probably).
+ for try := uint8(0); try < 5; try++ {
+ a16 := ip.As16()
+ hashData := a16[len(a16)-3:]
+ hashData[0] += try
+ tryPort := (32 << 10) | uint16(crc32.ChecksumIEEE(hashData))
+ ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, strconv.Itoa(int(tryPort))))
+ if err == nil {
+ return ln, nil
+ }
+ }
+ // Fall back to random ephemeral port.
+ return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
+}
+
+type peerAPIListener struct {
+ ln net.Listener
+ lb *LocalBackend
+ urlStr string
+ selfNode *tailcfg.Node
+}
+
+func (pln *peerAPIListener) Port() int {
+ ta, ok := pln.ln.Addr().(*net.TCPAddr)
+ if !ok {
+ return 0
+ }
+ return ta.Port
+}
+
+func (pln *peerAPIListener) serve() {
+ defer pln.ln.Close()
+ logf := pln.lb.logf
+ for {
+ c, err := pln.ln.Accept()
+ if errors.Is(err, net.ErrClosed) {
+ return
+ }
+ if err != nil {
+ logf("peerapi.Accept: %v", err)
+ return
+ }
+ ta, ok := c.RemoteAddr().(*net.TCPAddr)
+ if !ok {
+ c.Close()
+ logf("peerapi: unexpected RemoteAddr %#v", c.RemoteAddr())
+ continue
+ }
+ ipp, ok := netaddr.FromStdAddr(ta.IP, ta.Port, "")
+ if !ok {
+ logf("peerapi: bogus TCPAddr %#v", ta)
+ c.Close()
+ continue
+ }
+ peerNode, peerUser, ok := pln.lb.WhoIs(ipp)
+ if !ok {
+ logf("peerapi: unknown peer %v", ipp)
+ c.Close()
+ continue
+ }
+ h := &peerAPIHandler{
+ isSelf: pln.selfNode.User == peerNode.User,
+ remoteAddr: ipp,
+ peerNode: peerNode,
+ peerUser: peerUser,
+ lb: pln.lb,
+ }
+ httpServer := &http.Server{
+ Handler: h,
+ }
+ go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
+ }
+}
+
+type oneConnListener struct {
+ net.Listener
+ conn net.Conn
+}
+
+func (l *oneConnListener) Accept() (c net.Conn, err error) {
+ c = l.conn
+ if c == nil {
+ err = io.EOF
+ return
+ }
+ err = nil
+ l.conn = nil
+ return
+}
+
+func (l *oneConnListener) Close() error { return nil }
+
+// peerAPIHandler serves the Peer API for a source specific client.
+type peerAPIHandler struct {
+ remoteAddr netaddr.IPPort
+ isSelf bool // whether peerNode is owned by same user as this node
+ peerNode *tailcfg.Node // peerNode is who's making the request
+ peerUser tailcfg.UserProfile // profile of peerNode
+ lb *LocalBackend
+}
+
+func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ who := h.peerUser.DisplayName
+ fmt.Fprintf(w, `<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<body>
+<h1>Hello, %s (%v)</h1>
+This is my Tailscale device. Your device is %v.
+`, html.EscapeString(who), h.remoteAddr.IP, html.EscapeString(h.peerNode.ComputedName))
+
+ if h.isSelf {
+ fmt.Fprintf(w, "<p>You are the owner of this node.\n")
+ }
+}
diff --git a/ipn/ipnlocal/peerapi_macios_ext.go b/ipn/ipnlocal/peerapi_macios_ext.go
new file mode 100644
index 000000000..a75e18eed
--- /dev/null
+++ b/ipn/ipnlocal/peerapi_macios_ext.go
@@ -0,0 +1,54 @@
+// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin,redo ios,redo
+
+package ipnlocal
+
+import (
+ "fmt"
+ "log"
+ "net"
+ "strings"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+ "inet.af/netaddr"
+ "tailscale.com/net/interfaces"
+)
+
+func init() {
+ initListenConfig = initListenConfigNetworkExtension
+}
+
+// initListenConfigNetworkExtension configures nc for listening on IP
+// through the iOS/macOS Network/System Extension (Packet Tunnel
+// Provider) sandbox.
+func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *interfaces.State, tunIfName string) error {
+ tunIf, ok := st.Interface[tunIfName]
+ if !ok {
+ return fmt.Errorf("no interface with name %q", tunIfName)
+ }
+ nc.Control = func(network, address string, c syscall.RawConn) error {
+ var sockErr error
+ err := c.Control(func(fd uintptr) {
+
+ v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
+ proto := unix.IPPROTO_IP
+ opt := unix.IP_BOUND_IF
+ if v6 {
+ proto = unix.IPPROTO_IPV6
+ opt = unix.IPV6_BOUND_IF
+ }
+
+ sockErr = unix.SetsockoptInt(int(fd), proto, opt, tunIf.Index)
+ log.Printf("peerapi: bind(%q, %q) on index %v = %v", network, address, tunIf.Index, sockErr)
+ })
+ if err != nil {
+ return err
+ }
+ return sockErr
+ }
+ return nil
+}
diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go
index b89f7bb08..de9d208f7 100644
--- a/ipn/ipnstate/ipnstate.go
+++ b/ipn/ipnstate/ipnstate.go
@@ -26,7 +26,14 @@ import (
// Status represents the entire state of the IPN network.
type Status struct {
+ // Version is the daemon's long version (see version.Long).
+ Version string
+
+ // BackendState is an ipn.State string value:
+ // "NoState", "NeedsLogin", "NeedsMachineAuth", "Stopped",
+ // "Starting", "Running".
BackendState string
+
AuthURL string // current URL provided by control to authorize client
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Self *PeerStatus
@@ -80,6 +87,8 @@ type PeerStatus struct {
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
+ PeerAPIURL []string
+
// ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect
// to us. These nodes should be hidden by "tailscale status"
@@ -105,22 +114,16 @@ type StatusBuilder struct {
st Status
}
-func (sb *StatusBuilder) SetBackendState(v string) {
- sb.mu.Lock()
- defer sb.mu.Unlock()
- sb.st.BackendState = v
-}
-
-func (sb *StatusBuilder) SetAuthURL(v string) {
- sb.mu.Lock()
- defer sb.mu.Unlock()
- sb.st.AuthURL = v
-}
-
-func (sb *StatusBuilder) SetMagicDNSSuffix(v string) {
+// MutateStatus calls f with the status to mutate.
+//
+// It may not assume other fields of status are already populated, and
+// may not retain or write to the Status after f returns.
+//
+// MutateStatus acquires a lock so f must not call back into sb.
+func (sb *StatusBuilder) MutateStatus(f func(*Status)) {
sb.mu.Lock()
defer sb.mu.Unlock()
- sb.st.MagicDNSSuffix = v
+ f(&sb.st)
}
func (sb *StatusBuilder) Status() *Status {
@@ -130,11 +133,19 @@ func (sb *StatusBuilder) Status() *Status {
return &sb.st
}
-// SetSelfStatus sets the status of the local machine.
-func (sb *StatusBuilder) SetSelfStatus(ss *PeerStatus) {
+// MutateSelfStatus calls f with the PeerStatus of our own node to mutate.
+//
+// It may not assume other fields of status are already populated, and
+// may not retain or write to the Status after f returns.
+//
+// MutateStatus acquires a lock so f must not call back into sb.
+func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
sb.mu.Lock()
defer sb.mu.Unlock()
- sb.st.Self = ss
+ if sb.st.Self == nil {
+ sb.st.Self = new(PeerStatus)
+ }
+ f(sb.st.Self)
}
// AddUser adds a user profile to the status.
@@ -394,10 +405,18 @@ type PingResult struct {
Err string
LatencySeconds float64
- Endpoint string // ip:port if direct UDP was used
+ // Endpoint is the ip:port if direct UDP was used.
+ // It is not currently set for TSMP pings.
+ Endpoint string
+
+ // DERPRegionID is non-zero DERP region ID if DERP was used.
+ // It is not currently set for TSMP pings.
+ DERPRegionID int
- DERPRegionID int // non-zero if DERP was used
- DERPRegionCode string // three-letter airport/region code if DERP was used
+ // DERPRegionCode is the three-letter region code
+ // corresponding to DERPRegionID.
+ // It is not currently set for TSMP pings.
+ DERPRegionCode string
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
}
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index 4e0dba3da..169f18ba4 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -10,9 +10,11 @@ import (
"io"
"net/http"
"runtime"
+ "strconv"
"inet.af/netaddr"
"tailscale.com/ipn/ipnlocal"
+ "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
@@ -56,6 +58,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveWhoIs(w, r)
case "/localapi/v0/goroutines":
h.serveGoroutines(w, r)
+ case "/localapi/v0/status":
+ h.serveStatus(w, r)
default:
io.WriteString(w, "tailscaled\n")
}
@@ -109,3 +113,31 @@ func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write(buf)
}
+
+func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
+ if !h.PermitRead {
+ http.Error(w, "status access denied", http.StatusForbidden)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ var st *ipnstate.Status
+ if defBool(r.FormValue("peers"), true) {
+ st = h.b.Status()
+ } else {
+ st = h.b.StatusWithoutPeers()
+ }
+ e := json.NewEncoder(w)
+ e.SetIndent("", "\t")
+ e.Encode(st)
+}
+
+func defBool(a string, def bool) bool {
+ if a == "" {
+ return def
+ }
+ v, err := strconv.ParseBool(a)
+ if err != nil {
+ return def
+ }
+ return v
+}
diff --git a/ipn/message.go b/ipn/message.go
index 9b7783f98..905664c93 100644
--- a/ipn/message.go
+++ b/ipn/message.go
@@ -15,7 +15,7 @@ import (
"log"
"time"
- "golang.org/x/oauth2"
+ "tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/structs"
"tailscale.com/version"
@@ -56,7 +56,8 @@ type FakeExpireAfterArgs struct {
}
type PingArgs struct {
- IP string
+ IP string
+ UseTSMP bool
}
// Command is a command message that is JSON encoded and sent by a
@@ -76,7 +77,7 @@ type Command struct {
Quit *NoArgs
Start *StartArgs
StartLoginInteractive *NoArgs
- Login *oauth2.Token
+ Login *tailcfg.Oauth2Token
Logout *NoArgs
SetPrefs *SetPrefsArgs
SetWantRunning *bool
@@ -173,11 +174,8 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
if c := cmd.RequestEngineStatus; c != nil {
bs.b.RequestEngineStatus()
return nil
- } else if c := cmd.RequestStatus; c != nil {
- bs.b.RequestStatus()
- return nil
} else if c := cmd.Ping; c != nil {
- bs.b.Ping(c.IP)
+ bs.b.Ping(c.IP, c.UseTSMP)
return nil
}
@@ -299,7 +297,7 @@ func (bc *BackendClient) StartLoginInteractive() {
bc.send(Command{StartLoginInteractive: &NoArgs{}})
}
-func (bc *BackendClient) Login(token *oauth2.Token) {
+func (bc *BackendClient) Login(token *tailcfg.Oauth2Token) {
bc.send(Command{Login: token})
}
@@ -323,8 +321,11 @@ func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
}
-func (bc *BackendClient) Ping(ip string) {
- bc.send(Command{Ping: &PingArgs{IP: ip}})
+func (bc *BackendClient) Ping(ip string, useTSMP bool) {
+ bc.send(Command{Ping: &PingArgs{
+ IP: ip,
+ UseTSMP: useTSMP,
+ }})
}
func (bc *BackendClient) SetWantRunning(v bool) {
diff --git a/ipn/message_test.go b/ipn/message_test.go
index 4422a64ca..59a9eef25 100644
--- a/ipn/message_test.go
+++ b/ipn/message_test.go
@@ -10,7 +10,7 @@ import (
"testing"
"time"
- "golang.org/x/oauth2"
+ "tailscale.com/tailcfg"
"tailscale.com/tstest"
)
@@ -176,7 +176,7 @@ func TestClientServer(t *testing.T) {
h.Logout()
flushUntil(NeedsLogin)
- h.Login(&oauth2.Token{
+ h.Login(&tailcfg.Oauth2Token{
AccessToken: "google_id_token",
TokenType: GoogleIDTokenType,
})