m/n/k/authproxy: make use of SPDY through proxy work

Kubernetes still uses SPDY for interactive/streaming-type calls (like
exec or port-forward). Our proxy uses a HTTP/2 backend connection to
Kubernetes's API server. A HTTP/2 stream cannot be upgraded to SPDY
meaning these API requests all fail. This implements a slightly ugly
workaround by using two HTTP transports, a regular transport which
supports HTTP/2 and a fallback transport which does not. The proxy
selects the fallback transport if it detects that the request is trying
to upgrade to SPDY.

Change-Id: Idd44f58d07ec5570ddf8941ae7595225f47f254d
Reviewed-on: https://review.monogon.dev/c/monogon/+/645
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/kubernetes/authproxy/authproxy.go b/metropolis/node/kubernetes/authproxy/authproxy.go
index 0477b88..957cb8a 100644
--- a/metropolis/node/kubernetes/authproxy/authproxy.go
+++ b/metropolis/node/kubernetes/authproxy/authproxy.go
@@ -67,11 +67,16 @@
 		return err
 	}
 
-	proxy := httputil.NewSingleHostReverseProxy(&url.URL{
-		Host:   net.JoinHostPort("localhost", node.KubernetesAPIPort.PortString()),
+	internalAPIServer := net.JoinHostPort("localhost", node.KubernetesAPIPort.PortString())
+	standardProxy := httputil.NewSingleHostReverseProxy(&url.URL{
 		Scheme: "https",
+		Host:   internalAPIServer,
 	})
-	proxy.Transport = &http.Transport{
+	noHTTP2Proxy := httputil.NewSingleHostReverseProxy(&url.URL{
+		Scheme: "https",
+		Host:   internalAPIServer,
+	})
+	transport := &http.Transport{
 		DialContext: (&net.Dialer{
 			Timeout:   30 * time.Second,
 			KeepAlive: 30 * time.Second,
@@ -87,7 +92,12 @@
 		TLSHandshakeTimeout:   10 * time.Second,
 		ExpectContinueTimeout: 1 * time.Second,
 	}
-	proxy.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) {
+	standardProxy.Transport = transport
+	noHTTP2Transport := transport.Clone()
+	noHTTP2Transport.ForceAttemptHTTP2 = false
+	noHTTP2Transport.TLSClientConfig.NextProtos = []string{"http/1.1"}
+	noHTTP2Proxy.Transport = noHTTP2Transport
+	errorHandler := func(w http.ResponseWriter, req *http.Request, err error) {
 		logger.Infof("Proxy error: %v", err)
 		respondWithK8sStatus(w, &metav1.Status{
 			Status:  metav1.StatusFailure,
@@ -96,6 +106,8 @@
 			Message: "authproxy could not reach apiserver",
 		})
 	}
+	standardProxy.ErrorHandler = errorHandler
+	noHTTP2Proxy.ErrorHandler = errorHandler
 
 	serverCert, err := s.getTLSCert(ctx, pki.APIServer)
 	if err != nil {
@@ -130,6 +142,13 @@
 				})
 				return
 			}
+			proxyToUse := standardProxy
+			// Kubernetes wants to use SPDY but using SPDY with HTTP/2 is unsupported.
+			// SPDY should be removed from K8s, this is tracked in
+			// https://github.com/kubernetes/kubernetes/issues/7452
+			if strings.HasPrefix(strings.ToLower(req.Header.Get("Upgrade")), "spdy/") {
+				proxyToUse = noHTTP2Proxy
+			}
 			// Clone the request as otherwise modifying it is not allowed
 			newReq := req.Clone(req.Context())
 			// Drop any X-Remote headers to prevent injection
@@ -141,7 +160,7 @@
 			newReq.Header.Set("X-Remote-User", clientIdentity)
 			newReq.Header.Set("X-Remote-Group", "")
 
-			proxy.ServeHTTP(rw, newReq)
+			proxyToUse.ServeHTTP(rw, newReq)
 		}),
 	}
 	go server.ListenAndServeTLS("", "")