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, ` + +
+