|
|
@@ -6,6 +6,7 @@ package ipnlocal
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
+ "encoding/base64"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
@@ -33,6 +34,7 @@ import (
|
|
|
"tailscale.com/hostinfo"
|
|
|
"tailscale.com/ipn"
|
|
|
"tailscale.com/logtail/backoff"
|
|
|
+ "tailscale.com/net/dns/resolver"
|
|
|
"tailscale.com/net/interfaces"
|
|
|
"tailscale.com/syncs"
|
|
|
"tailscale.com/tailcfg"
|
|
|
@@ -48,6 +50,7 @@ type peerAPIServer struct {
|
|
|
tunName string
|
|
|
selfNode *tailcfg.Node
|
|
|
knownEmpty syncs.AtomicBool
|
|
|
+ resolver *resolver.Resolver
|
|
|
|
|
|
// directFileMode is whether we're writing files directly to a
|
|
|
// download directory (as *.partial files), rather than making
|
|
|
@@ -503,6 +506,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
h.handlePeerPut(w, r)
|
|
|
return
|
|
|
}
|
|
|
+ if strings.HasPrefix(r.URL.Path, "/dns-query") {
|
|
|
+ h.handleDNSQuery(w, r)
|
|
|
+ return
|
|
|
+ }
|
|
|
switch r.URL.Path {
|
|
|
case "/v0/goroutines":
|
|
|
h.handleServeGoroutines(w, r)
|
|
|
@@ -749,3 +756,64 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
clientmetric.WritePrometheusExpositionFormat(w)
|
|
|
}
|
|
|
+
|
|
|
+func (h *peerAPIHandler) replyToDNSQueries() bool {
|
|
|
+ // TODO(bradfitz): maybe lock this down more? what if we're an
|
|
|
+ // exit node but ACLs don't permit autogroup:internet access
|
|
|
+ // from h.peerNode via this node? peerapi bypasses ACL checks,
|
|
|
+ // so we should do additional checks here; but on what? this
|
|
|
+ // node's UDP port 53? our upstream DNS forwarder IP(s)?
|
|
|
+ // For now just offer DNS to any peer if we're an exit node.
|
|
|
+ return h.isSelf || h.ps.b.OfferingExitNode()
|
|
|
+}
|
|
|
+
|
|
|
+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
|
|
|
+ }
|
|
|
+ q, publicError := dohQuery(r)
|
|
|
+ if publicError != "" {
|
|
|
+ http.Error(w, publicError, http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // TODO(bradfitz): owl.
|
|
|
+ fmt.Fprintf(w, "## TODO: got %d bytes of DNS query", len(q))
|
|
|
+}
|
|
|
+
|
|
|
+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, ""
|
|
|
+ }
|
|
|
+}
|