Skip to content

Commit

Permalink
GEP-1911 - h2c backend protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
dprotaso committed Oct 15, 2023
1 parent a984578 commit 7770263
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 16 deletions.
8 changes: 7 additions & 1 deletion conformance/base/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,15 @@ spec:
selector:
app: infra-backend-v1
ports:
- protocol: TCP
- name: first-port
protocol: TCP
port: 8080
targetPort: 3000
- name: second-port
protocol: TCP
appProtocol: kubernetes.io/h2c
port: 8081
targetPort: 3001
---
apiVersion: apps/v1
kind: Deployment
Expand Down
83 changes: 83 additions & 0 deletions conformance/tests/httproute-backend-protocol-h2c.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package tests

import (
"testing"

"k8s.io/apimachinery/pkg/types"

"sigs.k8s.io/gateway-api/conformance/utils/http"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
"sigs.k8s.io/gateway-api/conformance/utils/roundtripper"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
)

func init() {
ConformanceTests = append(ConformanceTests, HTTPRouteBackendProtocolH2C)
}

var HTTPRouteBackendProtocolH2C = suite.ConformanceTest{
ShortName: "HTTPRouteBackendProtocolH2C",
Description: "A HTTPRoute with a BackendRef that has an appProtocol kubernetes.io/h2c should be functional",
Features: []suite.SupportedFeature{
suite.SupportGateway,
suite.SupportHTTPRoute,
suite.SupportHTTPRouteBackendProtocolH2C,
},
Manifests: []string{
"tests/httproute-backend-protocol-h2c.yaml",
},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "backend-protocol-h2c", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)

// TODO - client h2c upgrade flow
//
// Go's HTTP client is unable to handle the protocol change transparently see: https://github.com/golang/go/issues/46249
//
// t.Run("h2c upgrade request should reach backend", func(t *testing.T) {
// http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, http.ExpectedResponse{
// Request: http.Request{
// Path: "/",
// Headers: map[string]string{
// "Connection": "Upgrade, HTTP2-Settings",
// "Upgrade": "h2c",
// "HTTP2-Settings": "",
// },
// },
// Response: http.Response{StatusCode: 200},
// Backend: "infra-backend-v1",
// Namespace: "gateway-conformance-infra",
// })
// })

t.Run("http2 prior knowledge request should reach backend", func(t *testing.T) {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, http.ExpectedResponse{
Request: http.Request{
Path: "/",
Protocol: roundtripper.H2CPriorKnowledgeProtocol,
},
Response: http.Response{StatusCode: 200},
Backend: "infra-backend-v1",
Namespace: "gateway-conformance-infra",
})
})
},
}
17 changes: 17 additions & 0 deletions conformance/tests/httproute-backend-protocol-h2c.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: backend-protocol-h2c
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
rules:
- backendRefs:
# This points to a Service with the following ServicePort
# - protocol: TCP
# appProtocol: kubernetes.io/h2c
# port: 8081
# targetPort: 3001
- name: infra-backend-v1
port: 8081
6 changes: 5 additions & 1 deletion conformance/utils/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ func MakeRequest(t *testing.T, expected *ExpectedResponse, gwAddr, protocol, sch
expected.Response.StatusCode = 200
}

if expected.Request.Protocol == "" {
expected.Request.Protocol = protocol
}

path, query, _ := strings.Cut(expected.Request.Path, "?")
reqURL := url.URL{Scheme: scheme, Host: CalculateHost(t, gwAddr, scheme), Path: path, RawQuery: query}

Expand All @@ -125,7 +129,7 @@ func MakeRequest(t *testing.T, expected *ExpectedResponse, gwAddr, protocol, sch
Method: expected.Request.Method,
Host: expected.Request.Host,
URL: reqURL,
Protocol: protocol,
Protocol: expected.Request.Protocol,
Headers: map[string][]string{},
UnfollowRedirect: expected.Request.UnfollowRedirect,
}
Expand Down
73 changes: 59 additions & 14 deletions conformance/utils/roundtripper/roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net"
Expand All @@ -29,9 +30,14 @@ import (
"net/url"
"regexp"

"golang.org/x/net/http2"
"sigs.k8s.io/gateway-api/conformance/utils/config"
)

const (
H2CPriorKnowledgeProtocol = "H2C_PRIOR_KNOWLEDGE"
)

// RoundTripper is an interface used to make requests within conformance tests.
// This can be overridden with custom implementations whenever necessary.
type RoundTripper interface {
Expand Down Expand Up @@ -104,19 +110,7 @@ type DefaultRoundTripper struct {
CustomDialContext func(context.Context, string, string) (net.Conn, error)
}

// CaptureRoundTrip makes a request with the provided parameters and returns the
// captured request and response from echoserver. An error will be returned if
// there is an error running the function but not if an HTTP error status code
// is received.
func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedRequest, *CapturedResponse, error) {
client := &http.Client{}

if request.UnfollowRedirect {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}

func (d *DefaultRoundTripper) httpTransport(request Request) (http.RoundTripper, error) {
transport := &http.Transport{
DialContext: d.CustomDialContext,
// We disable keep-alives so that we don't leak established TCP connections.
Expand All @@ -131,10 +125,61 @@ func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedReques
if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 {
tlsConfig, err := tlsClientConfig(request.Server, request.CertPem, request.KeyPem)
if err != nil {
return nil, nil, err
return nil, err
}
transport.TLSClientConfig = tlsConfig
}

return transport, nil
}

func (d *DefaultRoundTripper) h2cPriorKnowledgeTransport(request Request) (http.RoundTripper, error) {
if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 {
return nil, errors.New("request has configured cert and key but h2 prior knowledge is not encrypted")
}

transport := &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, addr)
},
}

return transport, nil
}

// CaptureRoundTrip makes a request with the provided parameters and returns the
// captured request and response from echoserver. An error will be returned if
// there is an error running the function but not if an HTTP error status code
// is received.
func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedRequest, *CapturedResponse, error) {
var transport http.RoundTripper
var err error

switch request.Protocol {
case H2CPriorKnowledgeProtocol:
transport, err = d.h2cPriorKnowledgeTransport(request)
default:
transport, err = d.httpTransport(request)
}

if err != nil {
return nil, nil, err
}

return d.defaultRoundTrip(request, transport)
}

func (d *DefaultRoundTripper) defaultRoundTrip(request Request, transport http.RoundTripper) (*CapturedRequest, *CapturedResponse, error) {
client := &http.Client{}

if request.UnfollowRedirect {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}

client.Transport = transport

method := "GET"
Expand Down
4 changes: 4 additions & 0 deletions conformance/utils/suite/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ const (

// This option indicates support for HTTPRoute backendRequest timeouts (extended conformance).
SupportHTTPRouteBackendTimeout SupportedFeature = "HTTPRouteBackendTimeout"

// This option indicates support for HTTPRoute with a backendref with an appProtoocol 'kubernetes.io/h2c'
SupportHTTPRouteBackendProtocolH2C SupportedFeature = "HTTPRouteBackendProtocolH2C"
)

// HTTPRouteExtendedFeatures includes all the supported features for HTTPRoute
Expand Down Expand Up @@ -167,6 +170,7 @@ const (
// Implementations have the flexibility to opt-in for either specific features or the entire set.
var HTTPRouteExperimentalFeatures = sets.New(
SupportHTTPRouteDestinationPortMatching,
SupportHTTPRouteBackendProtocolH2C,
)

// -----------------------------------------------------------------------------
Expand Down

0 comments on commit 7770263

Please sign in to comment.