From 231b88cc518518a5ac49cd6e26ad556e06046185 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 29 Oct 2022 21:30:03 -0700 Subject: [PATCH] control/controlclient: add start of noise+http2 upgrade test Basic HTTP/2-over-noise client test. To be fleshed out in subsequent commits that add more functionality to the noise client. Updates #5972 Change-Id: I0178343523ef4ae8e8fc87bae53cbc81f4e32fde Signed-off-by: Brad Fitzpatrick --- control/controlclient/noise_test.go | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/control/controlclient/noise_test.go b/control/controlclient/noise_test.go index a97af4045..3c8f1c14d 100644 --- a/control/controlclient/noise_test.go +++ b/control/controlclient/noise_test.go @@ -5,10 +5,21 @@ package controlclient import ( + "context" + "encoding/binary" + "encoding/json" + "io" "math" + "net/http" + "net/http/httptest" "testing" + "golang.org/x/net/http2" + "tailscale.com/control/controlhttp" + "tailscale.com/net/tsdial" "tailscale.com/tailcfg" + "tailscale.com/types/key" + "tailscale.com/types/logger" ) // maxAllowedNoiseVersion is the highest we expect the Tailscale @@ -26,3 +37,117 @@ func TestNoiseVersion(t *testing.T) { t.Fatalf("tailcfg.CurrentCapabilityVersion is %d, want <=%d", tailcfg.CurrentCapabilityVersion, maxAllowedNoiseVersion) } } + +func TestNoiseClientHTTP2Upgrade(t *testing.T) { + serverPrivate := key.NewMachine() + clientPrivate := key.NewMachine() + + const msg = "Hello, client" + h2 := &http2.Server{} + hs := httptest.NewServer(&Upgrader{ + h2srv: h2, + noiseKeyPriv: serverPrivate, + httpBaseConfig: &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, msg) + }), + }, + }) + defer hs.Close() + + dialer := new(tsdial.Dialer) + nc, err := newNoiseClient(clientPrivate, serverPrivate.Public(), hs.URL, dialer, nil) + if err != nil { + t.Fatal(err) + } + res, err := nc.post(context.Background(), "/", nil) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + all, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if string(all) != msg { + t.Errorf("got response %q; want %q", all, msg) + } + +} + +// Upgrader is an http.Handler that hijacks and upgrades POST-with-Upgrade +// request to a Tailscale 2021 connection, then hands the resulting +// controlbase.Conn off to h2srv. +type Upgrader struct { + // h2srv is that will handle requests after the + // connection has been upgraded to HTTP/2-over-noise. + h2srv *http2.Server + + // httpBaseConfig is the http1 server config that h2srv is + // associated with. + httpBaseConfig *http.Server + + logf logger.Logf + + noiseKeyPriv key.MachinePrivate + + sendEarlyPayload bool +} + +func (up *Upgrader) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if up == nil || up.h2srv == nil { + http.Error(w, "invalid server config", http.StatusServiceUnavailable) + return + } + if r.URL.Path != "/ts2021" { + http.Error(w, "ts2021 upgrader installed at wrong path", http.StatusBadGateway) + return + } + if up.noiseKeyPriv.IsZero() { + http.Error(w, "keys not available", http.StatusServiceUnavailable) + return + } + + chalPub := key.NewChallenge() + earlyWriteFn := func(protocolVersion int, w io.Writer) error { + if !up.sendEarlyPayload { + return nil + } + earlyJSON, err := json.Marshal(struct { + NodeKeyOwnershipChallenge string + }{chalPub.Public().String()}) + if err != nil { + return err + } + // 5 bytes that won't be mistaken for an HTTP/2 frame: + // https://httpwg.org/specs/rfc7540.html#rfc.section.4.1 (Especially not + // an HTTP/2 settings frame, which isn't of type 'T') + var notH2Frame = [5]byte{0xff, 0xff, 0xff, 'T', 'S'} + var lenBuf [4]byte + binary.BigEndian.PutUint32(lenBuf[:], uint32(len(earlyJSON))) + // These writes are all buffered by caller, so fine to do them + // separately: + if _, err := w.Write(notH2Frame[:]); err != nil { + return err + } + if _, err := w.Write(lenBuf[:]); err != nil { + return err + } + if _, err := w.Write(earlyJSON[:]); err != nil { + return err + } + return nil + } + + cbConn, err := controlhttp.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn) + if err != nil { + up.logf("controlhttp: Accept: %v", err) + return + } + defer cbConn.Close() + + up.h2srv.ServeConn(cbConn, &http2.ServeConnOpts{ + BaseConfig: up.httpBaseConfig, + }) +}