// 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, `
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, "
There is another interface using the CGNAT range.
") } else if err != nil { fmt.Fprintf(w, "Could not check for CGNAT interfaces: %s
\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, "%v | ", v) } fmt.Fprint(w, "||
---|---|---|
%s | ", html.EscapeString(fmt.Sprintf("%v", v))) } if extras, err := interfaces.InterfaceDebugExtras(iface.Index); err == nil && extras != "" { fmt.Fprintf(w, "%s | ", html.EscapeString(extras)) } else if err != nil { fmt.Fprintf(w, "%s | ", html.EscapeString(err.Error())) } fmt.Fprint(w, "
") 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, "") } 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, "
Label | ") fmt.Fprintln(w, "Tx | ") fmt.Fprintln(w, "Rx | ") for _, iface := range interfaceStats.Interfaces { fmt.Fprintf(w, "Tx (%s) | ", html.EscapeString(iface)) fmt.Fprintf(w, "Rx (%s) | ", html.EscapeString(iface)) } fmt.Fprintln(w, "Validation | ") fmt.Fprintln(w, "") fmt.Fprintln(w, "") 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, "|
---|---|---|---|---|---|---|
%s | ", html.EscapeString(label.String())) fmt.Fprintf(w, "%d | ", stat.TxBytes) fmt.Fprintf(w, "%d | ", stat.RxBytes) txTotal += stat.TxBytes rxTotal += stat.RxBytes if interfaceStat, ok := interfaceStats.Stats[label]; ok { for _, iface := range interfaceStats.Interfaces { fmt.Fprintf(w, "%d | ", interfaceStat.TxBytesByInterface[iface]) fmt.Fprintf(w, "%d | ", 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, "Tx=%d (%+d) Rx=%d (%+d) | ", validationStat.TxBytes, int64(validationStat.TxBytes)-int64(stat.TxBytes), validationStat.RxBytes, int64(validationStat.RxBytes)-int64(stat.RxBytes)) } else { fmt.Fprintln(w, "") } fmt.Fprintln(w, " | Total | ") fmt.Fprintf(w, "%d | ", txTotal) fmt.Fprintf(w, "%d | ", rxTotal) for _, iface := range interfaceStats.Interfaces { fmt.Fprintf(w, "%d | ", txTotalByInterface[iface]) fmt.Fprintf(w, "%d | ", rxTotalByInterface[iface]) } fmt.Fprintln(w, "") fmt.Fprintln(w, "") fmt.Fprintln(w, " |
") fmt.Fprintln(w, html.EscapeString(sockstats.DebugInfo())) fmt.Fprintln(w, "") } // 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") )