Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GEP-1911] h2c backend protocol conformance #2456

Merged
merged 4 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
dprotaso marked this conversation as resolved.
Show resolved Hide resolved
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
66 changes: 66 additions & 0 deletions conformance/tests/httproute-backend-protocol-h2c.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
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)

// We are not testing the h2c HTTP upgrade mechanism as it is deprecated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha good to know

// See: https://datatracker.ietf.org/doc/html/rfc9113#versioning

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/v1
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
74 changes: 60 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,15 @@ 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 +111,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 +126,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 appProtocol '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