Skip to content

Commit a418627

Browse files
committed
Add an helper for proxied HTTP/2
The standard library's http.Server supports HTTP/2, but only for tls.Conn. This doesn't work when serving connections behind a reverse proxy which terminates TLS and uses the PROXY protocol. Supporting this requires some glue code, which the new helper provides. The example was tested with tlstunnel. Closes: #90
1 parent 864358a commit a418627

File tree

6 files changed

+363
-1
lines changed

6 files changed

+363
-1
lines changed

examples/httpserver/httpserver.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/pires/go-proxyproto"
10+
h2proxy "github.com/pires/go-proxyproto/helper/http2"
1011
)
1112

1213
// TODO: add httpclient example
@@ -35,5 +36,5 @@ func main() {
3536
}
3637
defer proxyListener.Close()
3738

38-
server.Serve(proxyListener)
39+
h2proxy.NewServer(&server, nil).Serve(proxyListener)
3940
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
module github.com/pires/go-proxyproto
22

33
go 1.18
4+
5+
require (
6+
golang.org/x/net v0.12.0 // indirect
7+
golang.org/x/text v0.11.0 // indirect
8+
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
2+
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
3+
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
4+
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=

helper/http2/http2.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Package http2 provides helpers for HTTP/2.
2+
package http2
3+
4+
import (
5+
"crypto/tls"
6+
"fmt"
7+
"log"
8+
"net"
9+
"net/http"
10+
"sync"
11+
"time"
12+
13+
"github.com/pires/go-proxyproto"
14+
"golang.org/x/net/http2"
15+
)
16+
17+
// Server is an HTTP server accepting both regular and proxied, both HTTP/1 and
18+
// HTTP/2 connections.
19+
//
20+
// HTTP/2 is negotiated using TLS ALPN, either directly via a tls.Conn, either
21+
// indirectly via the PROXY protocol.
22+
//
23+
// The server is closed when the http.Server is.
24+
type Server struct {
25+
h1 *http.Server
26+
h2 *http2.Server
27+
h2Err error
28+
h1Listener h1Listener
29+
30+
mu sync.Mutex
31+
closed bool
32+
listeners map[net.Listener]struct{}
33+
}
34+
35+
// NewServer creates a new HTTP server.
36+
//
37+
// A nil h2 is equivalent to a zero http2.Server.
38+
func NewServer(h1 *http.Server, h2 *http2.Server) *Server {
39+
if h2 == nil {
40+
h2 = new(http2.Server)
41+
}
42+
srv := &Server{
43+
h1: h1,
44+
h2: h2,
45+
h2Err: http2.ConfigureServer(h1, h2),
46+
listeners: make(map[net.Listener]struct{}),
47+
}
48+
srv.h1Listener = h1Listener{newPipeListener(), srv}
49+
go func() {
50+
// proxyListener.Accept never fails
51+
_ = h1.Serve(srv.h1Listener)
52+
}()
53+
return srv
54+
}
55+
56+
func (srv *Server) errorLog() *log.Logger {
57+
if srv.h1.ErrorLog != nil {
58+
return srv.h1.ErrorLog
59+
}
60+
return log.Default()
61+
}
62+
63+
// Serve accepts incoming connections on the listener l.
64+
func (srv *Server) Serve(ln net.Listener) error {
65+
if srv.h2Err != nil {
66+
return srv.h2Err
67+
}
68+
69+
srv.mu.Lock()
70+
ok := !srv.closed
71+
if ok {
72+
srv.listeners[ln] = struct{}{}
73+
}
74+
srv.mu.Unlock()
75+
if !ok {
76+
return http.ErrServerClosed
77+
}
78+
79+
defer func() {
80+
srv.mu.Lock()
81+
delete(srv.listeners, ln)
82+
srv.mu.Unlock()
83+
}()
84+
85+
var delay time.Duration
86+
for {
87+
conn, err := ln.Accept()
88+
if ne, ok := err.(net.Error); ok && ne.Timeout() {
89+
if delay == 0 {
90+
delay = 5 * time.Millisecond
91+
} else {
92+
delay *= 2
93+
}
94+
if max := 1 * time.Second; delay > max {
95+
delay = max
96+
}
97+
srv.errorLog().Printf("listener %q: accept error (retrying in %v): %v", ln.Addr(), delay, err)
98+
time.Sleep(delay)
99+
} else if err != nil {
100+
return fmt.Errorf("failed to accept connection: %w", err)
101+
}
102+
103+
delay = 0
104+
105+
go func() {
106+
if err := srv.serveConn(conn); err != nil {
107+
srv.errorLog().Printf("listener %q: %v", ln.Addr(), err)
108+
}
109+
}()
110+
}
111+
}
112+
113+
func (srv *Server) serveConn(conn net.Conn) error {
114+
var proto string
115+
switch conn := conn.(type) {
116+
case *tls.Conn:
117+
proto = conn.ConnectionState().NegotiatedProtocol
118+
case *proxyproto.Conn:
119+
if proxyHeader := conn.ProxyHeader(); proxyHeader != nil {
120+
tlvs, err := proxyHeader.TLVs()
121+
if err != nil {
122+
conn.Close()
123+
return err
124+
}
125+
for _, tlv := range tlvs {
126+
if tlv.Type == proxyproto.PP2_TYPE_ALPN {
127+
proto = string(tlv.Value)
128+
break
129+
}
130+
}
131+
}
132+
}
133+
134+
switch proto {
135+
case "h2", "h2c":
136+
defer conn.Close()
137+
opts := http2.ServeConnOpts{
138+
Handler: srv.h1.Handler,
139+
}
140+
srv.h2.ServeConn(conn, &opts)
141+
return nil
142+
case "", "http/1.0", "http/1.1":
143+
return srv.h1Listener.ServeConn(conn)
144+
default:
145+
conn.Close()
146+
return fmt.Errorf("unsupported protocol %q", proto)
147+
}
148+
}
149+
150+
func (srv *Server) closeListeners() error {
151+
srv.mu.Lock()
152+
defer srv.mu.Unlock()
153+
154+
srv.closed = true
155+
156+
var err error
157+
for ln := range srv.listeners {
158+
if cerr := ln.Close(); cerr != nil {
159+
err = cerr
160+
}
161+
}
162+
return err
163+
}
164+
165+
// h1Listener is used to signal back http.Server's Close and Shutdown to the
166+
// HTTP/2 server.
167+
type h1Listener struct {
168+
*pipeListener
169+
srv *Server
170+
}
171+
172+
func (ln h1Listener) Close() error {
173+
// pipeListener.Close never fails
174+
_ = ln.pipeListener.Close()
175+
return ln.srv.closeListeners()
176+
}

helper/http2/http2_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package http2_test
2+
3+
import (
4+
"errors"
5+
"log"
6+
"net"
7+
"net/http"
8+
"testing"
9+
10+
"github.com/pires/go-proxyproto"
11+
h2proxy "github.com/pires/go-proxyproto/helper/http2"
12+
"golang.org/x/net/http2"
13+
)
14+
15+
func ExampleServer() {
16+
ln, err := net.Listen("tcp", "localhost:80")
17+
if err != nil {
18+
log.Fatalf("failed to listen: %v", err)
19+
}
20+
21+
proxyLn := &proxyproto.Listener{
22+
Listener: ln,
23+
}
24+
25+
server := h2proxy.NewServer(&http.Server{
26+
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27+
_, _ = w.Write([]byte("Hello world!\n"))
28+
}),
29+
}, nil)
30+
if err := server.Serve(proxyLn); err != nil {
31+
log.Fatalf("failed to serve: %v", err)
32+
}
33+
}
34+
35+
func TestServer_h1(t *testing.T) {
36+
addr, server := newTestServer(t)
37+
defer server.Close()
38+
39+
resp, err := http.Get("http://" + addr)
40+
if err != nil {
41+
t.Fatalf("failed to perform HTTP request: %v", err)
42+
}
43+
resp.Body.Close()
44+
}
45+
46+
func TestServer_h2(t *testing.T) {
47+
addr, server := newTestServer(t)
48+
defer server.Close()
49+
50+
conn, err := net.Dial("tcp", addr)
51+
if err != nil {
52+
t.Fatalf("failed to dial: %v", err)
53+
}
54+
defer conn.Close()
55+
56+
proxyHeader := proxyproto.Header{
57+
Version: 2,
58+
Command: proxyproto.LOCAL,
59+
TransportProtocol: proxyproto.UNSPEC,
60+
}
61+
tlvs := []proxyproto.TLV{{
62+
Type: proxyproto.PP2_TYPE_ALPN,
63+
Value: []byte("h2"),
64+
}}
65+
if err := proxyHeader.SetTLVs(tlvs); err != nil {
66+
t.Fatalf("failed to set TLVs: %v", err)
67+
}
68+
if _, err := proxyHeader.WriteTo(conn); err != nil {
69+
t.Fatalf("failed to write PROXY header: %v", err)
70+
}
71+
72+
h2Conn, err := new(http2.Transport).NewClientConn(conn)
73+
if err != nil {
74+
t.Fatalf("failed to create HTTP connection: %v", err)
75+
}
76+
77+
req, err := http.NewRequest(http.MethodGet, "http://"+addr, nil)
78+
if err != nil {
79+
t.Fatalf("failed to create HTTP request: %v", err)
80+
}
81+
82+
resp, err := h2Conn.RoundTrip(req)
83+
if err != nil {
84+
t.Fatalf("failed to perform HTTP request: %v", err)
85+
}
86+
resp.Body.Close()
87+
}
88+
89+
func newTestServer(t *testing.T) (addr string, server *http.Server) {
90+
ln, err := net.Listen("tcp", "localhost:0")
91+
if err != nil {
92+
t.Fatalf("failed to listen: %v", err)
93+
}
94+
95+
server = &http.Server{
96+
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
97+
}),
98+
}
99+
100+
h2Server := h2proxy.NewServer(server, nil)
101+
go func() {
102+
err := h2Server.Serve(&proxyproto.Listener{Listener: ln})
103+
if err != nil && !errors.Is(err, net.ErrClosed) {
104+
t.Fatalf("failed to serve: %v", err)
105+
}
106+
}()
107+
108+
return ln.Addr().String(), server
109+
}

helper/http2/listener.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package http2
2+
3+
import (
4+
"net"
5+
"sync"
6+
)
7+
8+
// pipeListener is a hack to workaround the lack of http.Server.ServeConn.
9+
// See: https://github.com/golang/go/issues/36673
10+
type pipeListener struct {
11+
ch chan net.Conn
12+
closed bool
13+
mu sync.Mutex
14+
}
15+
16+
func newPipeListener() *pipeListener {
17+
return &pipeListener{
18+
ch: make(chan net.Conn, 64),
19+
}
20+
}
21+
22+
func (ln *pipeListener) Accept() (net.Conn, error) {
23+
conn, ok := <-ln.ch
24+
if !ok {
25+
return nil, net.ErrClosed
26+
}
27+
return conn, nil
28+
}
29+
30+
func (ln *pipeListener) Close() error {
31+
ln.mu.Lock()
32+
defer ln.mu.Unlock()
33+
34+
if ln.closed {
35+
return net.ErrClosed
36+
}
37+
ln.closed = true
38+
close(ln.ch)
39+
return nil
40+
}
41+
42+
// ServeConn enqueues a new connection. The connection will be returned in the
43+
// next Accept call.
44+
func (ln *pipeListener) ServeConn(conn net.Conn) error {
45+
ln.mu.Lock()
46+
defer ln.mu.Unlock()
47+
48+
if ln.closed {
49+
return net.ErrClosed
50+
}
51+
ln.ch <- conn
52+
return nil
53+
}
54+
55+
func (ln *pipeListener) Addr() net.Addr {
56+
return pipeAddr{}
57+
}
58+
59+
type pipeAddr struct{}
60+
61+
func (pipeAddr) Network() string {
62+
return "pipe"
63+
}
64+
65+
func (pipeAddr) String() string {
66+
return "pipe"
67+
}

0 commit comments

Comments
 (0)