diff --git a/internal/atunnel/ingress.go b/internal/atunnel/ingress.go index 9cf824eb1e..04baa49d67 100644 --- a/internal/atunnel/ingress.go +++ b/internal/atunnel/ingress.go @@ -96,6 +96,34 @@ type activation struct { wg sync.WaitGroup } +type upstreamTransport struct { + http1 *http.Transport + h2c *http.Transport +} + +func newUpstreamTransport() *upstreamTransport { + http1 := http.DefaultTransport.(*http.Transport).Clone() + h2c := http1.Clone() + + protocols := new(http.Protocols) + protocols.SetUnencryptedHTTP2(true) + h2c.Protocols = protocols + + return &upstreamTransport{http1: http1, h2c: h2c} +} + +func (t *upstreamTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.ProtoMajor == 2 { + return t.h2c.RoundTrip(req) + } + return t.http1.RoundTrip(req) +} + +func (t *upstreamTransport) CloseIdleConnections() { + t.http1.CloseIdleConnections() + t.h2c.CloseIdleConnections() +} + // NewServer creates a Server and validates its TLS material. func NewServer(cfg Config) (*Server, error) { if cfg.CredentialBundlePath == "" { @@ -126,7 +154,6 @@ func NewServer(cfg Config) (*Server, error) { return nil, fmt.Errorf("atunnel: trust bundle %q contains no certificates", cfg.TrustBundlePath) } - transport := http.DefaultTransport.(*http.Transport).Clone() proxy := &httputil.ReverseProxy{ Rewrite: func(pr *httputil.ProxyRequest) { pr.SetURL(cfg.Upstream) @@ -142,7 +169,7 @@ func NewServer(cfg Config) (*Server, error) { } pr.SetXForwarded() }, - Transport: transport, + Transport: newUpstreamTransport(), ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { slog.WarnContext(r.Context(), "atunnel upstream request failed", slog.Any("err", err)) http.Error(w, "bad gateway", http.StatusBadGateway) diff --git a/internal/atunnel/ingress_test.go b/internal/atunnel/ingress_test.go index a121a39fcb..70b5a490fe 100644 --- a/internal/atunnel/ingress_test.go +++ b/internal/atunnel/ingress_test.go @@ -190,6 +190,48 @@ func TestServeHTTP(t *testing.T) { } } +func TestServeHTTPPreservesProtocol(t *testing.T) { + gotProtocol := make(chan int, 1) + actor := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotProtocol <- r.ProtoMajor + w.WriteHeader(http.StatusNoContent) + })) + protocols := new(http.Protocols) + protocols.SetHTTP1(true) + protocols.SetUnencryptedHTTP2(true) + actor.Config.Protocols = protocols + actor.Start() + defer actor.Close() + + upstreamURL, err := url.Parse(actor.URL) + if err != nil { + t.Fatal(err) + } + s := newTestServer(t, upstreamURL) + if err := s.Activate("team-a", "actor-1"); err != nil { + t.Fatal(err) + } + + for _, tt := range []struct { + name string + protoMajor int + }{{"HTTP/1", 1}, {"HTTP/2", 2}} { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "https://worker/hello", nil) + req.Host = "actor-1.team-a.actors.resources.substrate.ate.dev" + req.ProtoMajor = tt.protoMajor + rec := httptest.NewRecorder() + s.ServeHTTP(rec, req) + if rec.Code != http.StatusNoContent { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent) + } + if got := <-gotProtocol; got != tt.protoMajor { + t.Errorf("actor protocol = HTTP/%d, want HTTP/%d", got, tt.protoMajor) + } + }) + } +} + func TestServeHTTPHonorsTargetPortHeader(t *testing.T) { upstreamURL, err := url.Parse("http://actor.internal:80") if err != nil {