diff options
Diffstat (limited to 'ipn')
| -rw-r--r-- | ipn/backend.go | 11 | ||||
| -rw-r--r-- | ipn/fake_test.go | 10 | ||||
| -rw-r--r-- | ipn/handle.go | 8 | ||||
| -rw-r--r-- | ipn/ipnlocal/local.go | 202 | ||||
| -rw-r--r-- | ipn/ipnlocal/local_test.go | 32 | ||||
| -rw-r--r-- | ipn/ipnlocal/peerapi.go | 167 | ||||
| -rw-r--r-- | ipn/ipnlocal/peerapi_macios_ext.go | 54 | ||||
| -rw-r--r-- | ipn/ipnstate/ipnstate.go | 59 | ||||
| -rw-r--r-- | ipn/localapi/localapi.go | 32 | ||||
| -rw-r--r-- | ipn/message.go | 21 | ||||
| -rw-r--r-- | ipn/message_test.go | 4 |
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, }) |
