From 81143b6d9aa8a2ea5d728617b7faf72217c5958a Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 25 Mar 2021 15:38:40 -0700 Subject: [PATCH] ipn/ipnlocal: start of peerapi between nodes Also some necessary refactoring of the ipn/ipnstate too. Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local.go | 65 +++++++++++---- ipn/ipnlocal/peerapi.go | 137 ++++++++++++++++++++++++++++++++ ipn/ipnstate/ipnstate.go | 44 +++++----- wgengine/magicsock/magicsock.go | 43 +++++----- 4 files changed, 229 insertions(+), 60 deletions(-) create mode 100644 ipn/ipnlocal/peerapi.go diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index a5bd272a7..c714bb933 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -95,15 +95,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(). @@ -237,16 +238,22 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) { func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) { b.mu.Lock() defer b.mu.Unlock() - sb.SetVersion(version.Long) - 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, "http://"+pln.ln.Addr().String()) + } + }) // TODO: hostinfo, and its networkinfo // TODO: EngineStatus copy (and deprecate it?) - if b.netMap != nil { - sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix()) - } - if extraLocked != nil { extraLocked(sb) } @@ -1426,6 +1433,32 @@ 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 + + for _, a := range b.netMap.Addresses { + ln, err := peerAPIListen(a.IP) + if err != nil { + b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err) + continue + } + pln := &peerAPIListener{ + ln: ln, + lb: b, + } + go pln.serve() + b.peerAPIListeners = append(b.peerAPIListeners, pln) + } } // magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS. diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go new file mode 100644 index 000000000..03e09896b --- /dev/null +++ b/ipn/ipnlocal/peerapi.go @@ -0,0 +1,137 @@ +// 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" + "strconv" + + "inet.af/netaddr" + "tailscale.com/tailcfg" +) + +var initListenConfig func(*net.ListenConfig, netaddr.IP) error + +func peerAPIListen(ip netaddr.IP) (ln net.Listener, err error) { + 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); err != nil { + return nil, err + } + } + // 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(), "tcp", net.JoinHostPort(ip.String(), strconv.Itoa(int(tryPort)))) + if err == nil { + return ln, nil + } + } + // Fall back to random ephemeral port. + return lc.Listen(context.Background(), "tcp", net.JoinHostPort(ip.String(), "0")) +} + +type peerAPIListener struct { + ln net.Listener + lb *LocalBackend +} + +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 + } + pas := &peerAPIServer{ + remoteAddr: ipp, + peerNode: peerNode, + peerUser: peerUser, + lb: pln.lb, + } + httpServer := &http.Server{ + Handler: pas, + } + 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 } + +type peerAPIServer struct { + remoteAddr netaddr.IPPort + peerNode *tailcfg.Node + peerUser tailcfg.UserProfile + lb *LocalBackend +} + +func (s *peerAPIServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + + +

Hello, %s (%v)

+This is my Tailscale device. Your device is %v. +`, html.EscapeString(s.peerUser.DisplayName), s.remoteAddr.IP, html.EscapeString(s.peerNode.ComputedName)) + +} diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index a61555760..de9d208f7 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -87,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" @@ -112,28 +114,16 @@ type StatusBuilder struct { st Status } -func (sb *StatusBuilder) SetVersion(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.Version = v -} - -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) { - sb.mu.Lock() - defer sb.mu.Unlock() - sb.st.MagicDNSSuffix = v + f(&sb.st) } func (sb *StatusBuilder) Status() *Status { @@ -143,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. diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 6fc26a2b5..7a437f9e8 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -2985,25 +2985,7 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { c.mu.Lock() defer c.mu.Unlock() - ss := &ipnstate.PeerStatus{ - PublicKey: c.privateKey.Public(), - Addrs: c.lastEndpoints, - OS: version.OS(), - } - if c.netMap != nil { - ss.HostName = c.netMap.Hostinfo.Hostname - ss.DNSName = c.netMap.Name - ss.UserID = c.netMap.User - } else { - ss.HostName, _ = os.Hostname() - } - if c.derpMap != nil { - derpRegion, ok := c.derpMap.Regions[c.myDerp] - if ok { - ss.Relay = derpRegion.RegionCode - } - } - + var tailAddr string if c.netMap != nil { for _, addr := range c.netMap.Addresses { if !addr.IsSingleIP() { @@ -3014,11 +2996,30 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { // readability of `tailscale status`, make it the IPv4 // address. if addr.IP.Is4() { - ss.TailAddr = addr.IP.String() + tailAddr = addr.IP.String() } } } - sb.SetSelfStatus(ss) + + sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) { + ss.PublicKey = c.privateKey.Public() + ss.Addrs = c.lastEndpoints + ss.OS = version.OS() + if c.netMap != nil { + ss.HostName = c.netMap.Hostinfo.Hostname + ss.DNSName = c.netMap.Name + ss.UserID = c.netMap.User + } else { + ss.HostName, _ = os.Hostname() + } + if c.derpMap != nil { + derpRegion, ok := c.derpMap.Regions[c.myDerp] + if ok { + ss.Relay = derpRegion.RegionCode + } + } + ss.TailAddr = tailAddr + }) for dk, n := range c.nodeOfDisco { ps := &ipnstate.PeerStatus{InMagicSock: true}