diff options
| author | Will Norris <will@tailscale.com> | 2022-11-23 09:54:48 -0800 |
|---|---|---|
| committer | Will Norris <will@tailscale.com> | 2022-11-23 10:02:22 -0800 |
| commit | f9fa174e11411d1d9ff8d7725acfefbc26f0688a (patch) | |
| tree | b60b0ed9ec44857d74983b9f04aa51bcc1b4c76b | |
| parent | b45b94877668666746a5a90176620e6bb6c686c8 (diff) | |
| download | tailscale-will/enforce-hostname.tar.xz tailscale-will/enforce-hostname.zip | |
tsweb: add EnforceHostname helper for DNS rebinding mitigationwill/enforce-hostname
Signed-off-by: Will Norris <will@tailscale.com>
| -rw-r--r-- | tsweb/tsweb.go | 66 |
1 files changed, 66 insertions, 0 deletions
diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go index 8b52e656d..7191d7d0a 100644 --- a/tsweb/tsweb.go +++ b/tsweb/tsweb.go @@ -25,9 +25,11 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "go4.org/mem" + "tailscale.com/client/tailscale" "tailscale.com/envknob" "tailscale.com/metrics" "tailscale.com/net/tsaddr" @@ -779,3 +781,67 @@ var ( _ PrometheusMetricsReflectRooter = expVarPromStructRoot{} _ expvar.Var = expVarPromStructRoot{} ) + +type hostEnforcer struct { + expiry time.Time + hosts []string + once sync.Once +} + +func (he *hostEnforcer) allowedHosts(ctx context.Context, client *tailscale.LocalClient) ([]string, error) { + var statusErr error + he.once.Do(func() { + status, err := client.StatusWithoutPeers(ctx) + if err != nil { + statusErr = err + return + } + dnsName := strings.TrimSuffix(status.Self.DNSName, ".") // node.tailnet.ts.net + bareHost := strings.TrimSuffix(dnsName, "."+status.MagicDNSSuffix) // node + he.hosts = []string{dnsName, bareHost} + }) + if statusErr != nil { + return nil, statusErr + } + return he.hosts, nil +} + +// EnforceHostname wraps a handler and enforces that all inbound requests have a Host header that matches the local Tailscale node. +// Acceptable Host headers are bare IP addresses, the node's full DNS name (node.tailnet.ts.net) and the node's bare hostname. +// This is used to help protect against DNS rebinding attacks. +// If client is nil, a default client will be used. +func EnforceHostname(h http.Handler, client *tailscale.LocalClient) http.Handler { + if client == nil { + client = &tailscale.LocalClient{} + } + enforcerAtomic := atomic.Pointer[hostEnforcer]{} + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // always allow bare IP addresses + if _, err := netip.ParseAddr(r.Host); err == nil { + h.ServeHTTP(w, r) + return + } + + enforcer := enforcerAtomic.Load() + // refresh the list of allowed hosts every 5 seconds + if now := time.Now(); enforcer == nil || now.After(enforcer.expiry) { + newEnforcer := &hostEnforcer{expiry: now.Add(5 * time.Second)} + if enforcerAtomic.CompareAndSwap(enforcer, newEnforcer) { + enforcer = enforcerAtomic.Load() + } + } + allowedHosts, err := enforcer.allowedHosts(r.Context(), client) + if err != nil { + http.Error(w, "unable to get tailscale status", http.StatusInternalServerError) + return + } + for _, host := range allowedHosts { + if r.Host == host { + h.ServeHTTP(w, r) + return + } + } + http.Error(w, "invalid Host header", http.StatusBadRequest) + }) +} |
