diff options
| author | Naman Sood <mail@nsood.in> | 2021-03-29 14:28:08 -0400 |
|---|---|---|
| committer | Naman Sood <mail@nsood.in> | 2021-03-29 14:28:08 -0400 |
| commit | c0a88a0129ebf0f9886b93b1f4e4f04a7c3bb86f (patch) | |
| tree | 57d5aef2985e3424e5bb6f4c810628aa3ccbf5d0 /ipn/ipnlocal | |
| parent | 47bd3c4cf5543fd7ecb049302c37c1001fa9f2d6 (diff) | |
| parent | a4c679e64691a3f0ba41ad9078312ca67e5e67fd (diff) | |
| download | tailscale-naman/netstack-subnet-routing.tar.xz tailscale-naman/netstack-subnet-routing.zip | |
merge with mainnaman/netstack-subnet-routing
Signed-off-by: Naman Sood <mail@nsood.in>
Diffstat (limited to 'ipn/ipnlocal')
| -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 |
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 +} |
