ipn/ipnlocal: start of peerapi between nodes
Also some necessary refactoring of the ipn/ipnstate too. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>pull/1593/head
parent
dad10fee9c
commit
81143b6d9a
|
@ -104,6 +104,7 @@ type LocalBackend struct {
|
||||||
authURL string
|
authURL string
|
||||||
interact bool
|
interact bool
|
||||||
prevIfState *interfaces.State
|
prevIfState *interfaces.State
|
||||||
|
peerAPIListeners []*peerAPIListener
|
||||||
|
|
||||||
// statusLock must be held before calling statusChanged.Wait() or
|
// statusLock must be held before calling statusChanged.Wait() or
|
||||||
// statusChanged.Broadcast().
|
// statusChanged.Broadcast().
|
||||||
|
@ -237,16 +238,22 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||||
func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) {
|
func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
sb.SetVersion(version.Long)
|
sb.MutateStatus(func(s *ipnstate.Status) {
|
||||||
sb.SetBackendState(b.state.String())
|
s.Version = version.Long
|
||||||
sb.SetAuthURL(b.authURL)
|
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: hostinfo, and its networkinfo
|
||||||
// TODO: EngineStatus copy (and deprecate it?)
|
// TODO: EngineStatus copy (and deprecate it?)
|
||||||
|
|
||||||
if b.netMap != nil {
|
|
||||||
sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix())
|
|
||||||
}
|
|
||||||
|
|
||||||
if extraLocked != nil {
|
if extraLocked != nil {
|
||||||
extraLocked(sb)
|
extraLocked(sb)
|
||||||
}
|
}
|
||||||
|
@ -1426,6 +1433,32 @@ func (b *LocalBackend) authReconfig() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
|
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.
|
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
||||||
|
|
|
@ -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, `<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(s.peerUser.DisplayName), s.remoteAddr.IP, html.EscapeString(s.peerNode.ComputedName))
|
||||||
|
|
||||||
|
}
|
|
@ -87,6 +87,8 @@ type PeerStatus struct {
|
||||||
KeepAlive bool
|
KeepAlive bool
|
||||||
ExitNode bool // true if this is the currently selected exit node.
|
ExitNode bool // true if this is the currently selected exit node.
|
||||||
|
|
||||||
|
PeerAPIURL []string
|
||||||
|
|
||||||
// ShareeNode indicates this node exists in the netmap because
|
// ShareeNode indicates this node exists in the netmap because
|
||||||
// it's owned by a shared-to user and that node might connect
|
// it's owned by a shared-to user and that node might connect
|
||||||
// to us. These nodes should be hidden by "tailscale status"
|
// to us. These nodes should be hidden by "tailscale status"
|
||||||
|
@ -112,28 +114,16 @@ type StatusBuilder struct {
|
||||||
st Status
|
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()
|
sb.mu.Lock()
|
||||||
defer sb.mu.Unlock()
|
defer sb.mu.Unlock()
|
||||||
sb.st.Version = v
|
f(&sb.st)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *StatusBuilder) Status() *Status {
|
func (sb *StatusBuilder) Status() *Status {
|
||||||
|
@ -143,11 +133,19 @@ func (sb *StatusBuilder) Status() *Status {
|
||||||
return &sb.st
|
return &sb.st
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSelfStatus sets the status of the local machine.
|
// MutateSelfStatus calls f with the PeerStatus of our own node to mutate.
|
||||||
func (sb *StatusBuilder) SetSelfStatus(ss *PeerStatus) {
|
//
|
||||||
|
// 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()
|
sb.mu.Lock()
|
||||||
defer sb.mu.Unlock()
|
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.
|
// AddUser adds a user profile to the status.
|
||||||
|
|
|
@ -2985,11 +2985,26 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
ss := &ipnstate.PeerStatus{
|
var tailAddr string
|
||||||
PublicKey: c.privateKey.Public(),
|
if c.netMap != nil {
|
||||||
Addrs: c.lastEndpoints,
|
for _, addr := range c.netMap.Addresses {
|
||||||
OS: version.OS(),
|
if !addr.IsSingleIP() {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
sb.AddTailscaleIP(addr.IP)
|
||||||
|
// TailAddr only allows for a single Tailscale IP. For
|
||||||
|
// readability of `tailscale status`, make it the IPv4
|
||||||
|
// address.
|
||||||
|
if addr.IP.Is4() {
|
||||||
|
tailAddr = addr.IP.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
|
||||||
|
ss.PublicKey = c.privateKey.Public()
|
||||||
|
ss.Addrs = c.lastEndpoints
|
||||||
|
ss.OS = version.OS()
|
||||||
if c.netMap != nil {
|
if c.netMap != nil {
|
||||||
ss.HostName = c.netMap.Hostinfo.Hostname
|
ss.HostName = c.netMap.Hostinfo.Hostname
|
||||||
ss.DNSName = c.netMap.Name
|
ss.DNSName = c.netMap.Name
|
||||||
|
@ -3003,22 +3018,8 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||||
ss.Relay = derpRegion.RegionCode
|
ss.Relay = derpRegion.RegionCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ss.TailAddr = tailAddr
|
||||||
if c.netMap != nil {
|
})
|
||||||
for _, addr := range c.netMap.Addresses {
|
|
||||||
if !addr.IsSingleIP() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sb.AddTailscaleIP(addr.IP)
|
|
||||||
// TailAddr only allows for a single Tailscale IP. For
|
|
||||||
// readability of `tailscale status`, make it the IPv4
|
|
||||||
// address.
|
|
||||||
if addr.IP.Is4() {
|
|
||||||
ss.TailAddr = addr.IP.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.SetSelfStatus(ss)
|
|
||||||
|
|
||||||
for dk, n := range c.nodeOfDisco {
|
for dk, n := range c.nodeOfDisco {
|
||||||
ps := &ipnstate.PeerStatus{InMagicSock: true}
|
ps := &ipnstate.PeerStatus{InMagicSock: true}
|
||||||
|
|
Loading…
Reference in New Issue