ipn/ipnlocal: proxy h2c grpc using net/http.Transport instead of x/net/http2

(Kinda related: #17351)

Updates #17305

Change-Id: I47df2612732a5713577164e74652bc9fa3cd14b3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-29 17:57:04 -07:00
committed by Brad Fitzpatrick
parent 3f5c560fd4
commit 2c956e30be
2 changed files with 98 additions and 12 deletions
+87 -1
View File
@@ -15,6 +15,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/netip"
@@ -881,7 +882,7 @@ func mustCreateURL(t *testing.T, u string) url.URL {
func newTestBackend(t *testing.T, opts ...any) *LocalBackend {
var logf logger.Logf = logger.Discard
const debug = true
const debug = false
if debug {
logf = logger.WithPrefix(tstest.WhileTestRunningLogger(t), "... ")
}
@@ -1085,3 +1086,88 @@ func TestEncTailscaleHeaderValue(t *testing.T) {
}
}
}
func TestServeGRPCProxy(t *testing.T) {
const msg = "some-response\n"
backend := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Path-Was", r.RequestURI)
w.Header().Set("Proto-Was", r.Proto)
io.WriteString(w, msg)
}))
backend.EnableHTTP2 = true
backend.Config.Protocols = new(http.Protocols)
backend.Config.Protocols.SetHTTP1(true)
backend.Config.Protocols.SetUnencryptedHTTP2(true)
backend.Start()
defer backend.Close()
backendURL := must.Get(url.Parse(backend.URL))
lb := newTestBackend(t)
rp := &reverseProxy{
logf: t.Logf,
url: backendURL,
backend: backend.URL,
lb: lb,
}
req := func(method, urlStr string, opt ...any) *http.Request {
req := httptest.NewRequest(method, urlStr, nil)
for _, o := range opt {
switch v := o.(type) {
case int:
req.ProtoMajor = v
case string:
req.Header.Set("Content-Type", v)
default:
panic(fmt.Sprintf("unsupported option type %T", v))
}
}
return req
}
tests := []struct {
name string
req *http.Request
wantPath string
wantProto string
wantBody string
}{
{
name: "non-gRPC",
req: req("GET", "http://foo/bar"),
wantPath: "/bar",
wantProto: "HTTP/1.1",
},
{
name: "gRPC-but-not-http2",
req: req("GET", "http://foo/bar", "application/grpc"),
wantPath: "/bar",
wantProto: "HTTP/1.1",
},
{
name: "gRPC--http2",
req: req("GET", "http://foo/bar", 2, "application/grpc"),
wantPath: "/bar",
wantProto: "HTTP/2.0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rec := httptest.NewRecorder()
rp.ServeHTTP(rec, tt.req)
res := rec.Result()
got := must.Get(io.ReadAll(res.Body))
if got, want := res.Header.Get("Path-Was"), tt.wantPath; want != got {
t.Errorf("Path-Was %q, want %q", got, want)
}
if got, want := res.Header.Get("Proto-Was"), tt.wantProto; want != got {
t.Errorf("Proto-Was %q, want %q", got, want)
}
if string(got) != msg {
t.Errorf("got body %q, want %q", got, msg)
}
})
}
}