| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package ipnlocal
- import (
- "context"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "hash/crc32"
- "html"
- "io"
- "net"
- "net/http"
- "net/netip"
- "net/url"
- "os"
- "runtime"
- "slices"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/kortschak/wol"
- "golang.org/x/net/dns/dnsmessage"
- "golang.org/x/net/http/httpguts"
- "tailscale.com/envknob"
- "tailscale.com/health"
- "tailscale.com/hostinfo"
- "tailscale.com/ipn"
- "tailscale.com/net/interfaces"
- "tailscale.com/net/netaddr"
- "tailscale.com/net/netutil"
- "tailscale.com/net/sockstats"
- "tailscale.com/tailcfg"
- "tailscale.com/taildrop"
- "tailscale.com/types/views"
- "tailscale.com/util/clientmetric"
- "tailscale.com/util/httphdr"
- "tailscale.com/wgengine/filter"
- )
- var initListenConfig func(*net.ListenConfig, netip.Addr, *interfaces.State, string) error
- // addH2C is non-nil on platforms where we want to add H2C
- // ("cleartext" HTTP/2) support to the peerAPI.
- var addH2C func(*http.Server)
- // peerDNSQueryHandler is implemented by tsdns.Resolver.
- type peerDNSQueryHandler interface {
- HandlePeerDNSQuery(context.Context, []byte, netip.AddrPort, func(name string) bool) (res []byte, err error)
- }
- type peerAPIServer struct {
- b *LocalBackend
- resolver peerDNSQueryHandler
- taildrop *taildrop.Manager
- }
- var (
- errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
- )
- func (s *peerAPIServer) listen(ip netip.Addr, ifState *interfaces.State) (ln net.Listener, err error) {
- // Android for whatever reason often has problems creating the peerapi listener.
- // But since we started intercepting it with netstack, it's not even important that
- // we have a real kernel-level listener. So just create a dummy listener on Android
- // and let netstack intercept it.
- if runtime.GOOS == "android" {
- return newFakePeerAPIListener(ip), nil
- }
- 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, s.b.dialer.TUNName()); err != nil {
- return nil, err
- }
- if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
- ipStr = ""
- }
- }
- if s.b.sys.IsNetstack() {
- 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 some random ephemeral port.
- ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
- // And if we're on a platform with netstack (anything but iOS), then just fallback to netstack.
- if err != nil && runtime.GOOS != "ios" {
- s.b.logf("peerapi: failed to do peerAPI listen, harmless (netstack available) but error was: %v", err)
- return newFakePeerAPIListener(ip), nil
- }
- return ln, err
- }
- type peerAPIListener struct {
- ps *peerAPIServer
- ip netip.Addr
- lb *LocalBackend
- // ln is the Listener. It can be nil in netstack mode if there are more than
- // 1 local addresses (e.g. both an IPv4 and IPv6). When it's nil, port
- // and urlStr are still populated.
- ln net.Listener
- // urlStr is the base URL to access the PeerAPI (http://ip:port/).
- urlStr string
- // port is just the port of urlStr.
- port int
- }
- func (pln *peerAPIListener) Close() error {
- if pln.ln != nil {
- return pln.ln.Close()
- }
- return nil
- }
- func (pln *peerAPIListener) serve() {
- if pln.ln == nil {
- return
- }
- 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 := netaddr.Unmap(ta.AddrPort())
- if !ipp.IsValid() {
- logf("peerapi: bogus TCPAddr %#v", ta)
- c.Close()
- continue
- }
- pln.ServeConn(ipp, c)
- }
- }
- func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
- logf := pln.lb.logf
- peerNode, peerUser, ok := pln.lb.WhoIs(src)
- if !ok {
- logf("peerapi: unknown peer %v", src)
- c.Close()
- return
- }
- nm := pln.lb.NetMap()
- if nm == nil || !nm.SelfNode.Valid() {
- logf("peerapi: no netmap")
- c.Close()
- return
- }
- h := &peerAPIHandler{
- ps: pln.ps,
- isSelf: nm.SelfNode.User() == peerNode.User(),
- remoteAddr: src,
- selfNode: nm.SelfNode,
- peerNode: peerNode,
- peerUser: peerUser,
- }
- httpServer := &http.Server{
- Handler: h,
- }
- if addH2C != nil {
- addH2C(httpServer)
- }
- go httpServer.Serve(netutil.NewOneConnListener(c, nil))
- }
- // peerAPIHandler serves the PeerAPI for a source specific client.
- type peerAPIHandler struct {
- ps *peerAPIServer
- remoteAddr netip.AddrPort
- isSelf bool // whether peerNode is owned by same user as this node
- selfNode tailcfg.NodeView // this node; always non-nil
- peerNode tailcfg.NodeView // peerNode is who's making the request
- peerUser tailcfg.UserProfile // profile of peerNode
- }
- func (h *peerAPIHandler) logf(format string, a ...any) {
- h.ps.b.logf("peerapi: "+format, a...)
- }
- // isAddressValid reports whether addr is a valid destination address for this
- // node originating from the peer.
- func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
- if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil {
- return *v == addr
- }
- if v := h.peerNode.SelfNodeV6MasqAddrForThisPeer(); v != nil {
- return *v == addr
- }
- pfx := netip.PrefixFrom(addr, addr.BitLen())
- return views.SliceContains(h.selfNode.Addresses(), pfx)
- }
- func (h *peerAPIHandler) validateHost(r *http.Request) error {
- if r.Host == "peer" {
- return nil
- }
- ap, err := netip.ParseAddrPort(r.Host)
- if err != nil {
- return err
- }
- if !h.isAddressValid(ap.Addr()) {
- return fmt.Errorf("%v not found in self addresses", ap.Addr())
- }
- return nil
- }
- func (h *peerAPIHandler) validatePeerAPIRequest(r *http.Request) error {
- if r.Referer() != "" {
- return errors.New("unexpected Referer")
- }
- if r.Header.Get("Origin") != "" {
- return errors.New("unexpected Origin")
- }
- return h.validateHost(r)
- }
- // peerAPIRequestShouldGetSecurityHeaders reports whether the PeerAPI request r
- // should get security response headers. It aims to report true for any request
- // from a browser and false for requests from tailscaled (Go) clients.
- //
- // PeerAPI is primarily an RPC mechanism between Tailscale instances. Some of
- // the HTTP handlers are useful for debugging with curl or browsers, but in
- // general the client is always tailscaled itself. Because PeerAPI only uses
- // HTTP/1 without HTTP/2 and its HPACK helping with repetitive headers, we try
- // to minimize header bytes sent in the common case when the client isn't a
- // browser. Minimizing bytes is important in particular with the ExitDNS service
- // provided by exit nodes, processing DNS clients from queries. We don't want to
- // waste bytes with security headers to non-browser clients. But if there's any
- // hint that the request is from a browser, then we do.
- func peerAPIRequestShouldGetSecurityHeaders(r *http.Request) bool {
- // Accept-Encoding is a forbidden header
- // (https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name)
- // that Chrome, Firefox, Safari, etc send, but Go does not. So if we see it,
- // it's probably a browser and not a Tailscale PeerAPI (Go) client.
- if httpguts.HeaderValuesContainsToken(r.Header["Accept-Encoding"], "deflate") {
- return true
- }
- // Clients can mess with their User-Agent, but if they say Mozilla or have a bunch
- // of components (spaces) they're likely a browser.
- if ua := r.Header.Get("User-Agent"); strings.HasPrefix(ua, "Mozilla/") || strings.Count(ua, " ") > 2 {
- return true
- }
- // Tailscale/PeerAPI/Go clients don't have an Accept-Language.
- if r.Header.Get("Accept-Language") != "" {
- return true
- }
- return false
- }
- func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if err := h.validatePeerAPIRequest(r); err != nil {
- metricInvalidRequests.Add(1)
- h.logf("invalid request from %v: %v", h.remoteAddr, err)
- http.Error(w, "invalid peerapi request", http.StatusForbidden)
- return
- }
- if peerAPIRequestShouldGetSecurityHeaders(r) {
- w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'; style-src 'unsafe-inline'`)
- w.Header().Set("X-Frame-Options", "DENY")
- w.Header().Set("X-Content-Type-Options", "nosniff")
- }
- if strings.HasPrefix(r.URL.Path, "/v0/put/") {
- if r.Method == "PUT" {
- metricPutCalls.Add(1)
- }
- h.handlePeerPut(w, r)
- return
- }
- if strings.HasPrefix(r.URL.Path, "/dns-query") {
- metricDNSCalls.Add(1)
- h.handleDNSQuery(w, r)
- return
- }
- switch r.URL.Path {
- case "/v0/goroutines":
- h.handleServeGoroutines(w, r)
- return
- case "/v0/env":
- h.handleServeEnv(w, r)
- return
- case "/v0/metrics":
- h.handleServeMetrics(w, r)
- return
- case "/v0/magicsock":
- h.handleServeMagicsock(w, r)
- return
- case "/v0/dnsfwd":
- h.handleServeDNSFwd(w, r)
- return
- case "/v0/wol":
- metricWakeOnLANCalls.Add(1)
- h.handleWakeOnLAN(w, r)
- return
- case "/v0/interfaces":
- h.handleServeInterfaces(w, r)
- return
- case "/v0/doctor":
- h.handleServeDoctor(w, r)
- case "/v0/sockstats":
- h.handleServeSockStats(w, r)
- return
- case "/v0/ingress":
- metricIngressCalls.Add(1)
- h.handleServeIngress(w, r)
- return
- }
- 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.Addr(), html.EscapeString(h.peerNode.ComputedName()))
- if h.isSelf {
- fmt.Fprintf(w, "<p>You are the owner of this node.\n")
- }
- }
- func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Request) {
- // http.Errors only useful if hitting endpoint manually
- // otherwise rely on log lines when debugging ingress connections
- // as connection is hijacked for bidi and is encrypted tls
- if !h.canIngress() {
- h.logf("ingress: denied; no ingress cap from %v", h.remoteAddr)
- http.Error(w, "denied; no ingress cap", http.StatusForbidden)
- return
- }
- logAndError := func(code int, publicMsg string) {
- h.logf("ingress: bad request from %v: %s", h.remoteAddr, publicMsg)
- http.Error(w, publicMsg, http.StatusMethodNotAllowed)
- }
- bad := func(publicMsg string) {
- logAndError(http.StatusBadRequest, publicMsg)
- }
- if r.Method != "POST" {
- logAndError(http.StatusMethodNotAllowed, "only POST allowed")
- return
- }
- srcAddrStr := r.Header.Get("Tailscale-Ingress-Src")
- if srcAddrStr == "" {
- bad("Tailscale-Ingress-Src header not set")
- return
- }
- srcAddr, err := netip.ParseAddrPort(srcAddrStr)
- if err != nil {
- bad("Tailscale-Ingress-Src header invalid; want ip:port")
- return
- }
- target := ipn.HostPort(r.Header.Get("Tailscale-Ingress-Target"))
- if target == "" {
- bad("Tailscale-Ingress-Target header not set")
- return
- }
- if _, _, err := net.SplitHostPort(string(target)); err != nil {
- bad("Tailscale-Ingress-Target header invalid; want host:port")
- return
- }
- getConnOrReset := func() (net.Conn, bool) {
- conn, _, err := w.(http.Hijacker).Hijack()
- if err != nil {
- h.logf("ingress: failed hijacking conn")
- http.Error(w, "failed hijacking conn", http.StatusInternalServerError)
- return nil, false
- }
- io.WriteString(conn, "HTTP/1.1 101 Switching Protocols\r\n\r\n")
- return &ipn.FunnelConn{
- Conn: conn,
- Src: srcAddr,
- Target: target,
- }, true
- }
- sendRST := func() {
- http.Error(w, "denied", http.StatusForbidden)
- }
- h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST)
- }
- func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprintln(w, "<h1>Interfaces</h1>")
- if dr, err := interfaces.DefaultRoute(); err == nil {
- fmt.Fprintf(w, "<h3>Default route is %q(%d)</h3>\n", html.EscapeString(dr.InterfaceName), dr.InterfaceIndex)
- } else {
- fmt.Fprintf(w, "<h3>Could not get the default route: %s</h3>\n", html.EscapeString(err.Error()))
- }
- if hasCGNATInterface, err := interfaces.HasCGNATInterface(); hasCGNATInterface {
- fmt.Fprintln(w, "<p>There is another interface using the CGNAT range.</p>")
- } else if err != nil {
- fmt.Fprintf(w, "<p>Could not check for CGNAT interfaces: %s</p>\n", html.EscapeString(err.Error()))
- }
- i, err := interfaces.GetList()
- if err != nil {
- fmt.Fprintf(w, "Could not get interfaces: %s\n", html.EscapeString(err.Error()))
- return
- }
- fmt.Fprintln(w, "<table style='border-collapse: collapse' border=1 cellspacing=0 cellpadding=2>")
- fmt.Fprint(w, "<tr>")
- for _, v := range []any{"Index", "Name", "MTU", "Flags", "Addrs", "Extra"} {
- fmt.Fprintf(w, "<th>%v</th> ", v)
- }
- fmt.Fprint(w, "</tr>\n")
- i.ForeachInterface(func(iface interfaces.Interface, ipps []netip.Prefix) {
- fmt.Fprint(w, "<tr>")
- for _, v := range []any{iface.Index, iface.Name, iface.MTU, iface.Flags, ipps} {
- fmt.Fprintf(w, "<td>%s</td> ", html.EscapeString(fmt.Sprintf("%v", v)))
- }
- if extras, err := interfaces.InterfaceDebugExtras(iface.Index); err == nil && extras != "" {
- fmt.Fprintf(w, "<td>%s</td> ", html.EscapeString(extras))
- } else if err != nil {
- fmt.Fprintf(w, "<td>%s</td> ", html.EscapeString(err.Error()))
- }
- fmt.Fprint(w, "</tr>\n")
- })
- fmt.Fprintln(w, "</table>")
- }
- func (h *peerAPIHandler) handleServeDoctor(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprintln(w, "<h1>Doctor Output</h1>")
- fmt.Fprintln(w, "<pre>")
- h.ps.b.Doctor(r.Context(), func(format string, args ...any) {
- line := fmt.Sprintf(format, args...)
- fmt.Fprintln(w, html.EscapeString(line))
- })
- fmt.Fprintln(w, "</pre>")
- }
- func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprintln(w, "<!DOCTYPE html><h1>Socket Stats</h1>")
- if !sockstats.IsAvailable {
- fmt.Fprintln(w, "Socket stats are not available for this client")
- return
- }
- stats, interfaceStats, validation := sockstats.Get(), sockstats.GetInterfaces(), sockstats.GetValidation()
- if stats == nil {
- fmt.Fprintln(w, "No socket stats available")
- return
- }
- fmt.Fprintln(w, "<table border='1' cellspacing='0' style='border-collapse: collapse;'>")
- fmt.Fprintln(w, "<thead>")
- fmt.Fprintln(w, "<th>Label</th>")
- fmt.Fprintln(w, "<th>Tx</th>")
- fmt.Fprintln(w, "<th>Rx</th>")
- for _, iface := range interfaceStats.Interfaces {
- fmt.Fprintf(w, "<th>Tx (%s)</th>", html.EscapeString(iface))
- fmt.Fprintf(w, "<th>Rx (%s)</th>", html.EscapeString(iface))
- }
- fmt.Fprintln(w, "<th>Validation</th>")
- fmt.Fprintln(w, "</thead>")
- fmt.Fprintln(w, "<tbody>")
- labels := make([]sockstats.Label, 0, len(stats.Stats))
- for label := range stats.Stats {
- labels = append(labels, label)
- }
- slices.SortFunc(labels, func(a, b sockstats.Label) int {
- return strings.Compare(a.String(), b.String())
- })
- txTotal := uint64(0)
- rxTotal := uint64(0)
- txTotalByInterface := map[string]uint64{}
- rxTotalByInterface := map[string]uint64{}
- for _, label := range labels {
- stat := stats.Stats[label]
- fmt.Fprintln(w, "<tr>")
- fmt.Fprintf(w, "<td>%s</td>", html.EscapeString(label.String()))
- fmt.Fprintf(w, "<td align=right>%d</td>", stat.TxBytes)
- fmt.Fprintf(w, "<td align=right>%d</td>", stat.RxBytes)
- txTotal += stat.TxBytes
- rxTotal += stat.RxBytes
- if interfaceStat, ok := interfaceStats.Stats[label]; ok {
- for _, iface := range interfaceStats.Interfaces {
- fmt.Fprintf(w, "<td align=right>%d</td>", interfaceStat.TxBytesByInterface[iface])
- fmt.Fprintf(w, "<td align=right>%d</td>", interfaceStat.RxBytesByInterface[iface])
- txTotalByInterface[iface] += interfaceStat.TxBytesByInterface[iface]
- rxTotalByInterface[iface] += interfaceStat.RxBytesByInterface[iface]
- }
- }
- if validationStat, ok := validation.Stats[label]; ok && (validationStat.RxBytes > 0 || validationStat.TxBytes > 0) {
- fmt.Fprintf(w, "<td>Tx=%d (%+d) Rx=%d (%+d)</td>",
- validationStat.TxBytes,
- int64(validationStat.TxBytes)-int64(stat.TxBytes),
- validationStat.RxBytes,
- int64(validationStat.RxBytes)-int64(stat.RxBytes))
- } else {
- fmt.Fprintln(w, "<td></td>")
- }
- fmt.Fprintln(w, "</tr>")
- }
- fmt.Fprintln(w, "</tbody>")
- fmt.Fprintln(w, "<tfoot>")
- fmt.Fprintln(w, "<th>Total</th>")
- fmt.Fprintf(w, "<th>%d</th>", txTotal)
- fmt.Fprintf(w, "<th>%d</th>", rxTotal)
- for _, iface := range interfaceStats.Interfaces {
- fmt.Fprintf(w, "<th>%d</th>", txTotalByInterface[iface])
- fmt.Fprintf(w, "<th>%d</th>", rxTotalByInterface[iface])
- }
- fmt.Fprintln(w, "<th></th>")
- fmt.Fprintln(w, "</tfoot>")
- fmt.Fprintln(w, "</table>")
- fmt.Fprintln(w, "<h2>Debug Info</h2>")
- fmt.Fprintln(w, "<pre>")
- fmt.Fprintln(w, html.EscapeString(sockstats.DebugInfo()))
- fmt.Fprintln(w, "</pre>")
- }
- // canPutFile reports whether h can put a file ("Taildrop") to this node.
- func (h *peerAPIHandler) canPutFile() bool {
- if h.peerNode.UnsignedPeerAPIOnly() {
- // Unsigned peers can't send files.
- return false
- }
- return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityFileSharingSend)
- }
- // canDebug reports whether h can debug this node (goroutines, metrics,
- // magicsock internal state, etc).
- func (h *peerAPIHandler) canDebug() bool {
- if !h.selfNode.HasCap(tailcfg.CapabilityDebug) {
- // This node does not expose debug info.
- return false
- }
- if h.peerNode.UnsignedPeerAPIOnly() {
- // Unsigned peers can't debug.
- return false
- }
- return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityDebugPeer)
- }
- // canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
- func (h *peerAPIHandler) canWakeOnLAN() bool {
- if h.peerNode.UnsignedPeerAPIOnly() {
- return false
- }
- return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN)
- }
- var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS")
- // canIngress reports whether h can send ingress requests to this node.
- func (h *peerAPIHandler) canIngress() bool {
- return h.peerHasCap(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf)
- }
- func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
- return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
- }
- func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
- if !h.canPutFile() {
- http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
- return
- }
- if !h.ps.b.hasCapFileSharing() {
- http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
- return
- }
- rawPath := r.URL.EscapedPath()
- prefix, ok := strings.CutPrefix(rawPath, "/v0/put/")
- if !ok {
- http.Error(w, "misconfigured internals", http.StatusForbidden)
- return
- }
- baseName, err := url.PathUnescape(prefix)
- if err != nil {
- http.Error(w, taildrop.ErrInvalidFileName.Error(), http.StatusBadRequest)
- return
- }
- enc := json.NewEncoder(w)
- switch r.Method {
- case "GET":
- id := taildrop.ClientID(h.peerNode.StableID())
- if prefix == "" {
- // List all the partial files.
- files, err := h.ps.taildrop.PartialFiles(id)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if err := enc.Encode(files); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- h.logf("json.Encoder.Encode error: %v", err)
- return
- }
- } else {
- // Stream all the block hashes for the specified file.
- next, close, err := h.ps.taildrop.HashPartialFile(id, baseName)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- defer close()
- for {
- switch cs, err := next(); {
- case err == io.EOF:
- return
- case err != nil:
- http.Error(w, err.Error(), http.StatusInternalServerError)
- h.logf("HashPartialFile.next error: %v", err)
- return
- default:
- if err := enc.Encode(cs); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- h.logf("json.Encoder.Encode error: %v", err)
- return
- }
- }
- }
- }
- case "PUT":
- t0 := h.ps.b.clock.Now()
- id := taildrop.ClientID(h.peerNode.StableID())
- var offset int64
- if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
- ranges, ok := httphdr.ParseRange(rangeHdr)
- if !ok || len(ranges) != 1 || ranges[0].Length != 0 {
- http.Error(w, "invalid Range header", http.StatusBadRequest)
- return
- }
- offset = ranges[0].Start
- }
- n, err := h.ps.taildrop.PutFile(taildrop.ClientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
- switch err {
- case nil:
- d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
- h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
- io.WriteString(w, "{}\n")
- case taildrop.ErrNoTaildrop:
- http.Error(w, err.Error(), http.StatusForbidden)
- case taildrop.ErrInvalidFileName:
- http.Error(w, err.Error(), http.StatusBadRequest)
- case taildrop.ErrFileExists:
- http.Error(w, err.Error(), http.StatusConflict)
- default:
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- default:
- http.Error(w, "expected method GET or PUT", http.StatusMethodNotAllowed)
- }
- }
- func approxSize(n int64) string {
- if n <= 1<<10 {
- return "<=1KB"
- }
- if n <= 1<<20 {
- return "<=1MB"
- }
- return fmt.Sprintf("~%dMB", n>>20)
- }
- func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- var buf []byte
- for size := 4 << 10; size <= 2<<20; size *= 2 {
- buf = make([]byte, size)
- buf = buf[:runtime.Stack(buf, true)]
- if len(buf) < size {
- break
- }
- }
- w.Write(buf)
- }
- func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- var data struct {
- Hostinfo *tailcfg.Hostinfo
- Uid int
- Args []string
- Env []string
- }
- data.Hostinfo = hostinfo.New()
- data.Uid = os.Getuid()
- data.Args = os.Args
- data.Env = os.Environ()
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(data)
- }
- func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- h.ps.b.MagicConn().ServeHTTPDebug(w, r)
- }
- func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- w.Header().Set("Content-Type", "text/plain")
- clientmetric.WritePrometheusExpositionFormat(w)
- }
- func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
- if !h.canDebug() {
- http.Error(w, "denied; no debug access", http.StatusForbidden)
- return
- }
- dh := health.DebugHandler("dnsfwd")
- if dh == nil {
- http.Error(w, "not wired up", http.StatusInternalServerError)
- return
- }
- dh.ServeHTTP(w, r)
- }
- func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request) {
- if !h.canWakeOnLAN() {
- http.Error(w, "no WoL access", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "bad method", http.StatusMethodNotAllowed)
- return
- }
- macStr := r.FormValue("mac")
- if macStr == "" {
- http.Error(w, "missing 'mac' param", http.StatusBadRequest)
- return
- }
- mac, err := net.ParseMAC(macStr)
- if err != nil {
- http.Error(w, "bad 'mac' param", http.StatusBadRequest)
- return
- }
- var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
- st := h.ps.b.sys.NetMon.Get().InterfaceState()
- if st == nil {
- http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
- return
- }
- var res struct {
- SentTo []string
- Errors []string
- }
- for ifName, ips := range st.InterfaceIPs {
- for _, ip := range ips {
- if ip.Addr().IsLoopback() || ip.Addr().Is6() {
- continue
- }
- local := &net.UDPAddr{
- IP: ip.Addr().AsSlice(),
- Port: 0,
- }
- remote := &net.UDPAddr{
- IP: net.IPv4bcast,
- Port: 0,
- }
- if err := wol.Wake(mac, password, local, remote); err != nil {
- res.Errors = append(res.Errors, err.Error())
- } else {
- res.SentTo = append(res.SentTo, ifName)
- }
- break // one per interface is enough
- }
- }
- sort.Strings(res.SentTo)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(res)
- }
- func (h *peerAPIHandler) replyToDNSQueries() bool {
- if h.isSelf {
- // If the peer is owned by the same user, just allow it
- // without further checks.
- return true
- }
- b := h.ps.b
- if !b.OfferingExitNode() && !b.OfferingAppConnector() {
- // If we're not an exit node or app connector, there's
- // no point to being a DNS server for somebody.
- return false
- }
- if !h.remoteAddr.IsValid() {
- // This should never be the case if the peerAPIHandler
- // was wired up correctly, but just in case.
- return false
- }
- // Otherwise, we're an exit node but the peer is not us, so
- // we need to check if they're allowed access to the internet.
- // As peerapi bypasses wgengine/filter checks, we need to check
- // ourselves. As a proxy for autogroup:internet access, we see
- // if we would've accepted a packet to 0.0.0.0:53. We treat
- // the IP 0.0.0.0 as being "the internet".
- //
- // Because of the way that filter checks work, rules are only
- // checked after ensuring the destination IP is part of the
- // local set of IPs. An exit node has 0.0.0.0/0 so its fine,
- // but an app connector explicitly adds 0.0.0.0/32 (and the
- // IPv6 equivalent) to make this work (see updateFilterLocked
- // in LocalBackend).
- f := b.filterAtomic.Load()
- if f == nil {
- return false
- }
- // Note: we check TCP here because the Filter type already had
- // a CheckTCP method (for unit tests), but it's pretty
- // arbitrary. DNS runs over TCP and UDP, so sure... we check
- // TCP.
- dstIP := netaddr.IPv4(0, 0, 0, 0)
- remoteIP := h.remoteAddr.Addr()
- if remoteIP.Is6() {
- // autogroup:internet for IPv6 is defined to start with 2000::/3,
- // so use 2000::0 as the probe "the internet" address.
- dstIP = netip.MustParseAddr("2000::")
- }
- verdict := f.CheckTCP(remoteIP, dstIP, 53)
- return verdict == filter.Accept
- }
- // handleDNSQuery implements a DoH server (RFC 8484) over the peerapi.
- // It's not over HTTPS as the spec dictates, but rather HTTP-over-WireGuard.
- func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
- if h.ps.resolver == nil {
- http.Error(w, "DNS not wired up", http.StatusNotImplemented)
- return
- }
- if !h.replyToDNSQueries() {
- http.Error(w, "DNS access denied", http.StatusForbidden)
- return
- }
- pretty := false // non-DoH debug mode for humans
- q, publicError := dohQuery(r)
- if publicError != "" && r.Method == "GET" {
- if name := r.FormValue("q"); name != "" {
- pretty = true
- publicError = ""
- q = dnsQueryForName(name, r.FormValue("t"))
- }
- }
- if publicError != "" {
- http.Error(w, publicError, http.StatusBadRequest)
- return
- }
- // Some timeout that's short enough to be noticed by humans
- // but long enough that it's longer than real DNS timeouts.
- const arbitraryTimeout = 5 * time.Second
- ctx, cancel := context.WithTimeout(r.Context(), arbitraryTimeout)
- defer cancel()
- res, err := h.ps.resolver.HandlePeerDNSQuery(ctx, q, h.remoteAddr, h.ps.b.allowExitNodeDNSProxyToServeName)
- if err != nil {
- h.logf("handleDNS fwd error: %v", err)
- if err := ctx.Err(); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- } else {
- http.Error(w, "DNS forwarding error", http.StatusInternalServerError)
- }
- return
- }
- // TODO(raggi): consider pushing the integration down into the resolver
- // instead to avoid re-parsing the DNS response for improved performance in
- // the future.
- if h.ps.b.OfferingAppConnector() {
- h.ps.b.ObserveDNSResponse(res)
- }
- if pretty {
- // Non-standard response for interactive debugging.
- w.Header().Set("Content-Type", "application/json")
- writePrettyDNSReply(w, res)
- return
- }
- w.Header().Set("Content-Type", "application/dns-message")
- w.Header().Set("Content-Length", strconv.Itoa(len(res)))
- w.Write(res)
- }
- func dohQuery(r *http.Request) (dnsQuery []byte, publicErr string) {
- const maxQueryLen = 256 << 10
- switch r.Method {
- default:
- return nil, "bad HTTP method"
- case "GET":
- q64 := r.FormValue("dns")
- if q64 == "" {
- return nil, "missing 'dns' parameter"
- }
- if base64.RawURLEncoding.DecodedLen(len(q64)) > maxQueryLen {
- return nil, "query too large"
- }
- q, err := base64.RawURLEncoding.DecodeString(q64)
- if err != nil {
- return nil, "invalid 'dns' base64 encoding"
- }
- return q, ""
- case "POST":
- if r.Header.Get("Content-Type") != "application/dns-message" {
- return nil, "unexpected Content-Type"
- }
- q, err := io.ReadAll(io.LimitReader(r.Body, maxQueryLen+1))
- if err != nil {
- return nil, "error reading post body with DNS query"
- }
- if len(q) > maxQueryLen {
- return nil, "query too large"
- }
- return q, ""
- }
- }
- func dnsQueryForName(name, typStr string) []byte {
- typ := dnsmessage.TypeA
- switch strings.ToLower(typStr) {
- case "aaaa":
- typ = dnsmessage.TypeAAAA
- case "txt":
- typ = dnsmessage.TypeTXT
- }
- b := dnsmessage.NewBuilder(nil, dnsmessage.Header{
- OpCode: 0, // query
- RecursionDesired: true,
- ID: 1, // arbitrary, but 0 is rejected by some servers
- })
- if !strings.HasSuffix(name, ".") {
- name += "."
- }
- b.StartQuestions()
- b.Question(dnsmessage.Question{
- Name: dnsmessage.MustNewName(name),
- Type: typ,
- Class: dnsmessage.ClassINET,
- })
- msg, _ := b.Finish()
- return msg
- }
- func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
- defer func() {
- if err != nil {
- j, _ := json.Marshal(struct {
- Error string
- }{err.Error()})
- j = append(j, '\n')
- w.Write(j)
- return
- }
- }()
- var p dnsmessage.Parser
- hdr, err := p.Start(res)
- if err != nil {
- return err
- }
- if hdr.RCode != dnsmessage.RCodeSuccess {
- return fmt.Errorf("DNS RCode = %v", hdr.RCode)
- }
- if err := p.SkipAllQuestions(); err != nil {
- return err
- }
- var gotIPs []string
- for {
- h, err := p.AnswerHeader()
- if err == dnsmessage.ErrSectionDone {
- break
- }
- if err != nil {
- return err
- }
- if h.Class != dnsmessage.ClassINET {
- continue
- }
- switch h.Type {
- case dnsmessage.TypeA:
- r, err := p.AResource()
- if err != nil {
- return err
- }
- gotIPs = append(gotIPs, net.IP(r.A[:]).String())
- case dnsmessage.TypeAAAA:
- r, err := p.AAAAResource()
- if err != nil {
- return err
- }
- gotIPs = append(gotIPs, net.IP(r.AAAA[:]).String())
- case dnsmessage.TypeTXT:
- r, err := p.TXTResource()
- if err != nil {
- return err
- }
- gotIPs = append(gotIPs, r.TXT...)
- }
- }
- j, _ := json.Marshal(gotIPs)
- j = append(j, '\n')
- w.Write(j)
- return nil
- }
- // newFakePeerAPIListener creates a new net.Listener that acts like
- // it's listening on the provided IP address and on TCP port 1.
- //
- // See docs on fakePeerAPIListener.
- func newFakePeerAPIListener(ip netip.Addr) net.Listener {
- return &fakePeerAPIListener{
- addr: net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, 1)),
- closed: make(chan struct{}),
- }
- }
- // fakePeerAPIListener is a net.Listener that has an Addr method returning a TCPAddr
- // for a given IP on port 1 (arbitrary) and can be Closed, but otherwise Accept
- // just blocks forever until closed. The purpose of this is to let the rest
- // of the LocalBackend/PeerAPI code run and think it's talking to the kernel,
- // even if the kernel isn't cooperating (like on Android: Issue 4449, 4293, etc)
- // or we lack permission to listen on a port. It's okay to not actually listen via
- // the kernel because on almost all platforms (except iOS as of 2022-04-20) we
- // also intercept incoming netstack TCP requests to our peerapi port and hand them over
- // directly to peerapi, without involving the kernel. So this doesn't need to be
- // real. But the port number we return (1, in this case) is the port number we advertise
- // to peers and they connect to. 1 seems pretty safe to use. Even if the kernel's
- // using it, it doesn't matter, as we intercept it first in netstack and the kernel
- // never notices.
- //
- // Eventually we'll remove this code and do this on all platforms, when iOS also uses
- // netstack.
- type fakePeerAPIListener struct {
- addr net.Addr
- closeOnce sync.Once
- closed chan struct{}
- }
- func (fl *fakePeerAPIListener) Close() error {
- fl.closeOnce.Do(func() { close(fl.closed) })
- return nil
- }
- func (fl *fakePeerAPIListener) Accept() (net.Conn, error) {
- <-fl.closed
- return nil, net.ErrClosed
- }
- func (fl *fakePeerAPIListener) Addr() net.Addr { return fl.addr }
- var (
- metricInvalidRequests = clientmetric.NewCounter("peerapi_invalid_requests")
- // Non-debug PeerAPI endpoints.
- metricPutCalls = clientmetric.NewCounter("peerapi_put")
- metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
- metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
- metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
- )
|