Skip to content

Commit ec0d124

Browse files
committed
GEP-1911 - h2c backend protocol
1 parent a984578 commit ec0d124

File tree

5 files changed

+214
-14
lines changed

5 files changed

+214
-14
lines changed

conformance/base/manifests.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ spec:
9494
- protocol: TCP
9595
port: 8080
9696
targetPort: 3000
97+
- protocol: TCP
98+
appProtocol: kubernetes.io/h2c
99+
port: 8081
100+
targetPort: 3001
97101
---
98102
apiVersion: apps/v1
99103
kind: Deployment
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tests
18+
19+
import (
20+
"testing"
21+
22+
"k8s.io/apimachinery/pkg/types"
23+
24+
"sigs.k8s.io/gateway-api/conformance/utils/http"
25+
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
26+
"sigs.k8s.io/gateway-api/conformance/utils/roundtripper"
27+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
28+
)
29+
30+
func init() {
31+
ConformanceTests = append(ConformanceTests, HTTPRouteBackendProtocolH2C)
32+
}
33+
34+
var HTTPRouteBackendProtocolH2C = suite.ConformanceTest{
35+
ShortName: "HTTPRouteBackendProtocolH2C",
36+
Description: "A HTTPRoute with a BackendRef that has an appProtocol kubernetes.io/h2c should be functional",
37+
Features: []suite.SupportedFeature{
38+
suite.SupportGateway,
39+
suite.SupportHTTPRoute,
40+
suite.SupportHTTPRouteBackendProtocolH2C,
41+
},
42+
Manifests: []string{
43+
"tests/httproute-backend-protocol-h2c.yaml",
44+
},
45+
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
46+
ns := "gateway-conformance-infra"
47+
routeNN := types.NamespacedName{Name: "backend-protocol-h2c", Namespace: ns}
48+
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
49+
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
50+
51+
t.Run("http2 prior knowledge request should reach backend", func(t *testing.T) {
52+
expected := http.ExpectedResponse{
53+
Response: http.Response{StatusCode: 200},
54+
Backend: "infra-backend-v1",
55+
Namespace: "gateway-conformance-infra",
56+
}
57+
58+
req := http.MakeRequest(t, &expected, gwAddr, roundtripper.H2CPriorKnowledgeProtocol, "http")
59+
60+
http.WaitForConsistentResponse(t, suite.RoundTripper, req, expected,
61+
suite.TimeoutConfig.RequiredConsecutiveSuccesses,
62+
suite.TimeoutConfig.MaxTimeToConsistency)
63+
})
64+
65+
t.Run("h2c upgrade request should reach backend", func(t *testing.T) {
66+
expected := http.ExpectedResponse{
67+
Response: http.Response{StatusCode: 200},
68+
Backend: "infra-backend-v1",
69+
Namespace: "gateway-conformance-infra",
70+
}
71+
72+
req := http.MakeRequest(t, &expected, gwAddr, roundtripper.H2CUpgradeProtocol, "http")
73+
http.WaitForConsistentResponse(t, suite.RoundTripper, req, expected,
74+
suite.TimeoutConfig.RequiredConsecutiveSuccesses,
75+
suite.TimeoutConfig.MaxTimeToConsistency)
76+
77+
})
78+
},
79+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: gateway.networking.k8s.io/v1beta1
2+
kind: HTTPRoute
3+
metadata:
4+
name: backend-protocol-h2c
5+
namespace: gateway-conformance-infra
6+
spec:
7+
parentRefs:
8+
- name: same-namespace
9+
rules:
10+
- backendRefs:
11+
# This points to a Service with the following ServicePort
12+
# - protocol: TCP
13+
# appProtocol: kubernetes.io/h2c
14+
# port: 8081
15+
# targetPort: 3001
16+
- name: infra-backend-v1
17+
port: 8081

conformance/utils/roundtripper/roundtripper.go

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ limitations under the License.
1717
package roundtripper
1818

1919
import (
20+
"bufio"
2021
"context"
2122
"crypto/tls"
2223
"crypto/x509"
2324
"encoding/json"
25+
"errors"
2426
"fmt"
2527
"io"
2628
"net"
@@ -29,9 +31,15 @@ import (
2931
"net/url"
3032
"regexp"
3133

34+
"golang.org/x/net/http2"
3235
"sigs.k8s.io/gateway-api/conformance/utils/config"
3336
)
3437

38+
const (
39+
H2CUpgradeProtocol = "H2C_UPGRADE"
40+
H2CPriorKnowledgeProtocol = "H2C_PRIOR_KNOWLEDGE"
41+
)
42+
3543
// RoundTripper is an interface used to make requests within conformance tests.
3644
// This can be overridden with custom implementations whenever necessary.
3745
type RoundTripper interface {
@@ -104,19 +112,7 @@ type DefaultRoundTripper struct {
104112
CustomDialContext func(context.Context, string, string) (net.Conn, error)
105113
}
106114

107-
// CaptureRoundTrip makes a request with the provided parameters and returns the
108-
// captured request and response from echoserver. An error will be returned if
109-
// there is an error running the function but not if an HTTP error status code
110-
// is received.
111-
func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedRequest, *CapturedResponse, error) {
112-
client := &http.Client{}
113-
114-
if request.UnfollowRedirect {
115-
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
116-
return http.ErrUseLastResponse
117-
}
118-
}
119-
115+
func (d *DefaultRoundTripper) httpTransport(request Request) (http.RoundTripper, error) {
120116
transport := &http.Transport{
121117
DialContext: d.CustomDialContext,
122118
// We disable keep-alives so that we don't leak established TCP connections.
@@ -131,10 +127,110 @@ func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedReques
131127
if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 {
132128
tlsConfig, err := tlsClientConfig(request.Server, request.CertPem, request.KeyPem)
133129
if err != nil {
134-
return nil, nil, err
130+
return nil, err
135131
}
136132
transport.TLSClientConfig = tlsConfig
137133
}
134+
135+
return transport, nil
136+
}
137+
138+
func (d *DefaultRoundTripper) h2cPriorKnowledgeTransport(request Request) (http.RoundTripper, error) {
139+
if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 {
140+
return nil, errors.New("request has configured cert and key but h2 prior knowledge is not encrypted")
141+
}
142+
143+
transport := &http2.Transport{
144+
AllowHTTP: true,
145+
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
146+
var d net.Dialer
147+
return d.DialContext(ctx, network, addr)
148+
},
149+
}
150+
151+
return transport, nil
152+
}
153+
154+
// This actually makes a H2C upgrade request when dialing to establish
155+
// an HTTP/2 connection. Then the transport will re-send the same request
156+
//
157+
// This seemed like the easiest way to accomplish testing h2c upgrade flow
158+
// with golang. Open issue is here https://github.com/golang/go/issues/46249
159+
//
160+
// The alternative would be to re-implement parts of http2.Transport
161+
func (d *DefaultRoundTripper) h2cUpgradeTransport(request Request) (http.RoundTripper, error) {
162+
if request.Server != "" && len(request.CertPem) != 0 && len(request.KeyPem) != 0 {
163+
return nil, errors.New("request has configured cert and key but h2c is not encrypted")
164+
}
165+
return &http2.Transport{
166+
AllowHTTP: true,
167+
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
168+
dialer := net.Dialer{}
169+
conn, err := dialer.DialContext(ctx, network, addr)
170+
if err != nil {
171+
return nil, err
172+
}
173+
bw := bufio.NewWriter(conn)
174+
br := bufio.NewReader(conn)
175+
176+
req, _ := http.NewRequestWithContext(ctx, request.Method, "http://"+addr, nil)
177+
req.Header.Set("Connection", "Upgrade, HTTP2-Settings")
178+
req.Header.Set("Upgrade", "h2c")
179+
req.Header.Set("HTTP2-Settings", "")
180+
181+
if err = req.Write(bw); err != nil {
182+
return nil, err
183+
}
184+
if err = bw.Flush(); err != nil {
185+
return nil, err
186+
}
187+
188+
resp, err := http.ReadResponse(br, req)
189+
if err != nil {
190+
return nil, err
191+
}
192+
defer resp.Body.Close()
193+
if resp.StatusCode != http.StatusSwitchingProtocols {
194+
return nil, errors.New("switching protocols failed")
195+
}
196+
return conn, nil
197+
},
198+
}, nil
199+
}
200+
201+
// CaptureRoundTrip makes a request with the provided parameters and returns the
202+
// captured request and response from echoserver. An error will be returned if
203+
// there is an error running the function but not if an HTTP error status code
204+
// is received.
205+
func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedRequest, *CapturedResponse, error) {
206+
var transport http.RoundTripper
207+
var err error
208+
209+
switch request.Protocol {
210+
case H2CUpgradeProtocol:
211+
transport, err = d.h2cUpgradeTransport(request)
212+
case H2CPriorKnowledgeProtocol:
213+
transport, err = d.h2cPriorKnowledgeTransport(request)
214+
default:
215+
transport, err = d.httpTransport(request)
216+
}
217+
218+
if err != nil {
219+
return nil, nil, err
220+
}
221+
222+
return d.defaultRoundTrip(request, transport)
223+
}
224+
225+
func (d *DefaultRoundTripper) defaultRoundTrip(request Request, transport http.RoundTripper) (*CapturedRequest, *CapturedResponse, error) {
226+
client := &http.Client{}
227+
228+
if request.UnfollowRedirect {
229+
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
230+
return http.ErrUseLastResponse
231+
}
232+
}
233+
138234
client.Transport = transport
139235

140236
method := "GET"

conformance/utils/suite/features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ const (
133133

134134
// This option indicates support for HTTPRoute backendRequest timeouts (extended conformance).
135135
SupportHTTPRouteBackendTimeout SupportedFeature = "HTTPRouteBackendTimeout"
136+
137+
// This option indicates support for HTTPRoute with a backendref with an appProtoocol 'kubernetes.io/h2c'
138+
SupportHTTPRouteBackendProtocolH2C SupportedFeature = "HTTPRouteBackendProtocolH2C"
136139
)
137140

138141
// HTTPRouteExtendedFeatures includes all the supported features for HTTPRoute
@@ -167,6 +170,7 @@ const (
167170
// Implementations have the flexibility to opt-in for either specific features or the entire set.
168171
var HTTPRouteExperimentalFeatures = sets.New(
169172
SupportHTTPRouteDestinationPortMatching,
173+
SupportHTTPRouteBackendProtocolH2C,
170174
)
171175

172176
// -----------------------------------------------------------------------------

0 commit comments

Comments
 (0)