summaryrefslogtreecommitdiffhomepage
path: root/ipn/ipnlocal
diff options
context:
space:
mode:
authorNaman Sood <mail@nsood.in>2021-03-29 14:28:08 -0400
committerNaman Sood <mail@nsood.in>2021-03-29 14:28:08 -0400
commitc0a88a0129ebf0f9886b93b1f4e4f04a7c3bb86f (patch)
tree57d5aef2985e3424e5bb6f4c810628aa3ccbf5d0 /ipn/ipnlocal
parent47bd3c4cf5543fd7ecb049302c37c1001fa9f2d6 (diff)
parenta4c679e64691a3f0ba41ad9078312ca67e5e67fd (diff)
downloadtailscale-naman/netstack-subnet-routing.tar.xz
tailscale-naman/netstack-subnet-routing.zip
Signed-off-by: Naman Sood <mail@nsood.in>
Diffstat (limited to 'ipn/ipnlocal')
-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
4 files changed, 394 insertions, 61 deletions
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
+}