Skip to content

Commit e052456

Browse files
Adds h2c support
Enables HTTP/2 connections over cleartext TCP with Prior Knowledge (RFC 7540 3.4). The implementation is based on the golang.org/x/net/http2/h2c and workarounds several issues: * golang/go#38064 * golang/go#26682 See h2c package docs for details. Signed-off-by: Alexander Yastrebov <[email protected]>
1 parent 8444aeb commit e052456

File tree

7 files changed

+288
-25
lines changed

7 files changed

+288
-25
lines changed

LICENSE

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Skipper is in general licensed under the following Apache Version 2.0 license with the exception
2-
of the pathmux subdirectory which is licensed under MIT license (see notice file below).
2+
of the pathmux subdirectory which is licensed under MIT license and
3+
h2c subdirectory which is licensed under BSD-style license (see notice files below).
34

45
Copyright 2015 Zalando SE
56

@@ -41,3 +42,34 @@ MODIFICATIONS TO pathmux/tree.go and pathmux/tree_test.go:
4142
it can be used to look up arbitrary objects in a Patricia tree.
4243

4344
21.04.2016 - Enabled backtracking in the tree lookup.
45+
46+
47+
Notice file for h2c/h2c.go
48+
49+
Copyright (c) 2009 The Go Authors. All rights reserved.
50+
51+
Redistribution and use in source and binary forms, with or without
52+
modification, are permitted provided that the following conditions are
53+
met:
54+
55+
* Redistributions of source code must retain the above copyright
56+
notice, this list of conditions and the following disclaimer.
57+
* Redistributions in binary form must reproduce the above
58+
copyright notice, this list of conditions and the following disclaimer
59+
in the documentation and/or other materials provided with the
60+
distribution.
61+
* Neither the name of Google Inc. nor the names of its
62+
contributors may be used to endorse or promote products derived from
63+
this software without specific prior written permission.
64+
65+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
66+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
67+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
68+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
69+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
70+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
71+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
72+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
73+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
74+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
75+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ type Config struct {
226226
ExpectContinueTimeoutBackend time.Duration `yaml:"expect-continue-timeout-backend"`
227227
MaxIdleConnsBackend int `yaml:"max-idle-connection-backend"`
228228
DisableHTTPKeepalives bool `yaml:"disable-http-keepalives"`
229+
EnableH2CPriorKnowledge bool `yaml:"enable-h2c-prior-knowledge"`
229230

230231
// swarm:
231232
EnableSwarm bool `yaml:"enable-swarm"`
@@ -474,6 +475,7 @@ func NewConfig() *Config {
474475
flag.DurationVar(&cfg.ExpectContinueTimeoutBackend, "expect-continue-timeout-backend", 30*time.Second, "sets the HTTP expect continue timeout for backend connections")
475476
flag.IntVar(&cfg.MaxIdleConnsBackend, "max-idle-connection-backend", 0, "sets the maximum idle connections for all backend connections")
476477
flag.BoolVar(&cfg.DisableHTTPKeepalives, "disable-http-keepalives", false, "forces backend to always create a new connection")
478+
flag.BoolVar(&cfg.EnableH2CPriorKnowledge, "enable-h2c-prior-knowledge", false, "enables HTTP/2 connections over cleartext TCP with Prior Knowledge")
477479

478480
// Swarm:
479481
flag.BoolVar(&cfg.EnableSwarm, "enable-swarm", false, "enable swarm communication between nodes in a skipper fleet")
@@ -805,6 +807,7 @@ func (c *Config) ToOptions() skipper.Options {
805807
ExpectContinueTimeoutBackend: c.ExpectContinueTimeoutBackend,
806808
MaxIdleConnsBackend: c.MaxIdleConnsBackend,
807809
DisableHTTPKeepalives: c.DisableHTTPKeepalives,
810+
EnableH2CPriorKnowledge: c.EnableH2CPriorKnowledge,
808811

809812
// swarm:
810813
EnableSwarm: c.EnableSwarm,

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ require (
4747
github.com/yuin/gopher-lua v0.0.0-20200603152657-dc2b0ca8b37e
4848
go.uber.org/atomic v1.4.0 // indirect
4949
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
50-
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d
50+
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167
5151
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
5252
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
5353
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect
54+
golang.org/x/text v0.3.7 // indirect
5455
golang.org/x/tools v0.1.0 // indirect
5556
google.golang.org/grpc v1.22.0 // indirect
5657
gopkg.in/alecthomas/kingpin.v2 v2.2.6
@@ -97,7 +98,6 @@ require (
9798
github.com/tidwall/pretty v1.2.0 // indirect
9899
github.com/tklauser/numcpus v0.2.2 // indirect
99100
go.opentelemetry.io/otel v0.13.0 // indirect
100-
golang.org/x/text v0.3.6 // indirect
101101
google.golang.org/appengine v1.5.0 // indirect
102102
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 // indirect
103103
google.golang.org/protobuf v1.23.0 // indirect

go.sum

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
378378
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
379379
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
380380
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
381-
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs=
382-
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
381+
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167 h1:eDd+TJqbgfXruGQ5sJRU7tEtp/58OAx4+Ayjxg4SM+4=
382+
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
383383
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
384384
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
385385
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -422,7 +422,7 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
422422
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
423423
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
424424
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
425-
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
425+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
426426
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
427427
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
428428
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -431,8 +431,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
431431
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
432432
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
433433
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
434-
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
435434
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
435+
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
436+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
436437
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
437438
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
438439
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

h2c/h2c.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package h2c implements the unencrypted "h2c" form of HTTP/2.
6+
//
7+
// The h2c protocol is the non-TLS version of HTTP/2
8+
//
9+
// The implementation is based on the golang.org/x/net/http2/h2c
10+
package h2c
11+
12+
import (
13+
"context"
14+
"errors"
15+
"io"
16+
"net"
17+
"net/http"
18+
"strings"
19+
"sync/atomic"
20+
"time"
21+
22+
log "github.com/sirupsen/logrus"
23+
"golang.org/x/net/http2"
24+
)
25+
26+
type Handler interface {
27+
// Shutdown gracefully shuts down the underlying HTTP/1 server and
28+
// waits for h2c connections to close.
29+
//
30+
// Returns HTTP/1 server shutdown err or the context's error
31+
// if the provided context expires before the shutdown is complete.
32+
Shutdown(context.Context) error
33+
}
34+
35+
type Options struct{}
36+
37+
type h2cHandler struct {
38+
handler http.Handler
39+
s1 *http.Server
40+
s2 *http2.Server
41+
conns int64
42+
}
43+
44+
// Enable creates an http2.Server s2, wraps http.Server s1 original handler
45+
// with a handler intercepting any h2c traffic and registers s2
46+
// startGracefulShutdown on s1 Shutdown.
47+
// It returns h2c handler that should be called to shutdown s1 and h2c connections.
48+
//
49+
// If a request is an h2c connection, it is hijacked and redirected to the
50+
// s2.ServeConn along with the original s1 handler. Otherwise the handler just
51+
// forwards requests to the original s1 handler. This works because h2c is
52+
// designed to be parsable as a valid HTTP/1, but ignored by any HTTP server
53+
// that does not handle h2c. Therefore we leverage the HTTP/1 compatible parts
54+
// of the Go http library to parse and recognize h2c requests.
55+
//
56+
// There are two ways to begin an h2c connection (RFC 7540 Section 3.2 and 3.4):
57+
// (1) Starting with Prior Knowledge - this works by starting an h2c connection
58+
// with a string of bytes that is valid HTTP/1, but unlikely to occur in
59+
// practice and (2) Upgrading from HTTP/1 to h2c.
60+
//
61+
// This implementation workarounds several issues of the golang.org/x/net/http2/h2c:
62+
// * drops support for upgrading from HTTP/1 to h2c, see https://github.com/golang/go/issues/38064
63+
// * implements graceful shutdown, see https://github.com/golang/go/issues/26682
64+
// * remove closing of the hijacked connection because s2.ServeConn closes it
65+
// * removes buffered connection write
66+
func Enable(s1 *http.Server, reserved *Options) Handler {
67+
s2 := &http2.Server{}
68+
h := &h2cHandler{handler: s1.Handler, s1: s1, s2: s2}
69+
70+
// register s2 startGracefulShutdown on s1 Shutdown
71+
http2.ConfigureServer(s1, s2)
72+
s1.Handler = h
73+
74+
return h
75+
}
76+
77+
func (h *h2cHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
78+
// Handle h2c with prior knowledge (RFC 7540 Section 3.4)
79+
if r.Method == "PRI" && len(r.Header) == 0 && r.URL.Path == "*" && r.Proto == "HTTP/2.0" {
80+
conn, err := initH2CWithPriorKnowledge(w)
81+
if err != nil {
82+
return
83+
}
84+
n := atomic.AddInt64(&h.conns, 1)
85+
log.Debugf("h2c start: %d connections", n)
86+
87+
h.s2.ServeConn(conn, &http2.ServeConnOpts{Handler: h.handler, BaseConfig: h.s1})
88+
89+
n = atomic.AddInt64(&h.conns, -1)
90+
log.Debugf("h2c done: %d connections", n)
91+
} else {
92+
h.handler.ServeHTTP(w, r)
93+
}
94+
}
95+
96+
// initH2CWithPriorKnowledge implements creating an h2c connection with prior
97+
// knowledge (Section 3.4) and creates a net.Conn suitable for http2.ServeConn.
98+
// All we have to do is look for the client preface that is supposed to be a part
99+
// of the body, and reforward the client preface on the net.Conn this function
100+
// creates.
101+
func initH2CWithPriorKnowledge(w http.ResponseWriter) (net.Conn, error) {
102+
hijacker, ok := w.(http.Hijacker)
103+
if !ok {
104+
return nil, errors.New("hijack is not supported")
105+
}
106+
conn, rw, err := hijacker.Hijack()
107+
if err != nil {
108+
return nil, err
109+
}
110+
r := rw.Reader
111+
112+
const expectedBody = "SM\r\n\r\n"
113+
114+
buf := make([]byte, len(expectedBody))
115+
n, err := io.ReadFull(r, buf)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
if string(buf[:n]) == expectedBody {
121+
return &h2cConn{
122+
Conn: conn,
123+
Reader: io.MultiReader(strings.NewReader(http2.ClientPreface), r),
124+
}, nil
125+
}
126+
127+
conn.Close()
128+
return nil, errors.New("invalid client preface")
129+
}
130+
131+
func (h *h2cHandler) Shutdown(ctx context.Context) error {
132+
serr := h.
133+
s1.
134+
Shutdown(ctx)
135+
136+
timer := time.NewTicker(500 * time.Millisecond)
137+
defer timer.Stop()
138+
for {
139+
n := atomic.LoadInt64(&h.conns)
140+
log.Debugf("h2c shutdown: %d connections", n)
141+
142+
if n == 0 {
143+
return serr
144+
}
145+
select {
146+
case <-ctx.Done():
147+
return ctx.Err()
148+
case <-timer.C:
149+
}
150+
}
151+
}
152+
153+
type h2cConn struct {
154+
net.Conn
155+
io.Reader
156+
}
157+
158+
func (c *h2cConn) Read(p []byte) (int, error) {
159+
return c.Reader.Read(p)
160+
}

skipper.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/zalando/skipper/filters/fadein"
3636
logfilter "github.com/zalando/skipper/filters/log"
3737
ratelimitfilters "github.com/zalando/skipper/filters/ratelimit"
38+
"github.com/zalando/skipper/h2c"
3839
"github.com/zalando/skipper/innkeeper"
3940
"github.com/zalando/skipper/loadbalancer"
4041
"github.com/zalando/skipper/logging"
@@ -343,6 +344,9 @@ type Options struct {
343344
// a backend to always create a new connection.
344345
DisableHTTPKeepalives bool
345346

347+
// Enables HTTP/2 connections over cleartext TCP with Prior Knowledge
348+
EnableH2CPriorKnowledge bool
349+
346350
// Flag indicating to ignore trailing slashes in paths during route
347351
// lookup.
348352
IgnoreTrailingSlash bool
@@ -981,6 +985,10 @@ func (o *Options) tlsConfig() (*tls.Config, error) {
981985
return nil, nil
982986
}
983987

988+
if o.EnableH2CPriorKnowledge {
989+
return nil, fmt.Errorf("TLS implies no HTTP/2 connections over cleartext TCP")
990+
}
991+
984992
crts := strings.Split(o.CertPathTLS, ",")
985993
keys := strings.Split(o.KeyPathTLS, ",")
986994

@@ -1064,6 +1072,7 @@ func listenAndServeQuit(
10641072
if err != nil {
10651073
return err
10661074
}
1075+
serveTLS := tlsConfig != nil
10671076

10681077
srv := &http.Server{
10691078
Addr: o.Address,
@@ -1094,6 +1103,12 @@ func listenAndServeQuit(
10941103
sigs = make(chan os.Signal, 1)
10951104
}
10961105

1106+
shutdown := srv.Shutdown
1107+
if o.EnableH2CPriorKnowledge {
1108+
log.Infof("Enabling HTTP/2 connections over cleartext TCP")
1109+
shutdown = h2c.Enable(srv, nil).Shutdown
1110+
}
1111+
10971112
go func() {
10981113
signal.Notify(sigs, syscall.SIGTERM)
10991114

@@ -1103,15 +1118,15 @@ func listenAndServeQuit(
11031118
time.Sleep(o.WaitForHealthcheckInterval)
11041119

11051120
log.Info("Start shutdown")
1106-
if err := srv.Shutdown(context.Background()); err != nil {
1121+
if err := shutdown(context.Background()); err != nil {
11071122
log.Errorf("Failed to graceful shutdown: %v", err)
11081123
}
11091124
close(idleConnsCH)
11101125
}()
11111126

11121127
log.Infof("proxy listener on %v", o.Address)
11131128

1114-
if srv.TLSConfig != nil {
1129+
if serveTLS {
11151130
if err := srv.ListenAndServeTLS("", ""); err != http.ErrServerClosed {
11161131
log.Errorf("ListenAndServeTLS failed: %v", err)
11171132
return err

0 commit comments

Comments
 (0)