mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
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 <bradfitz@tailscale.com>
This commit is contained in:
parent
e25ab75795
commit
231b88cc51
@ -5,10 +5,21 @@
|
|||||||
package controlclient
|
package controlclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"tailscale.com/control/controlhttp"
|
||||||
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxAllowedNoiseVersion is the highest we expect the Tailscale
|
// 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)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user