-
-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
6 changed files
with
368 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
module github.com/pires/go-proxyproto | ||
|
||
go 1.18 | ||
|
||
require ( | ||
golang.org/x/net v0.12.0 // indirect | ||
golang.org/x/text v0.11.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= | ||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= | ||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= | ||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
// Package http2 provides helpers for HTTP/2. | ||
package http2 | ||
|
||
import ( | ||
"crypto/tls" | ||
"fmt" | ||
"log" | ||
"net" | ||
"net/http" | ||
"sync" | ||
"time" | ||
|
||
"github.com/pires/go-proxyproto" | ||
"golang.org/x/net/http2" | ||
) | ||
|
||
// Server is an HTTP server accepting both regular and proxied, both HTTP/1 and | ||
// HTTP/2 connections. | ||
// | ||
// HTTP/2 is negotiated using TLS ALPN, either directly via a tls.Conn, either | ||
// indirectly via the PROXY protocol. | ||
// | ||
// The server is closed when the http.Server is. | ||
type Server struct { | ||
h1 *http.Server | ||
h2 *http2.Server | ||
h2Err error | ||
h1Listener h1Listener | ||
|
||
mu sync.Mutex | ||
closed bool | ||
listeners map[net.Listener]struct{} | ||
} | ||
|
||
// NewServer creates a new HTTP server. | ||
// | ||
// A nil h2 is equivalent to a zero http2.Server. | ||
func NewServer(h1 *http.Server, h2 *http2.Server) *Server { | ||
if h2 == nil { | ||
h2 = new(http2.Server) | ||
} | ||
srv := &Server{ | ||
h1: h1, | ||
h2: h2, | ||
h2Err: http2.ConfigureServer(h1, h2), | ||
listeners: make(map[net.Listener]struct{}), | ||
} | ||
srv.h1Listener = h1Listener{newPipeListener(), srv} | ||
go func() { | ||
// proxyListener.Accept never fails | ||
_ = h1.Serve(srv.h1Listener) | ||
}() | ||
return srv | ||
} | ||
|
||
func (srv *Server) errorLog() *log.Logger { | ||
if srv.h1.ErrorLog != nil { | ||
return srv.h1.ErrorLog | ||
} | ||
return log.Default() | ||
} | ||
|
||
// Serve accepts incoming connections on the listener l. | ||
func (srv *Server) Serve(ln net.Listener) error { | ||
if srv.h2Err != nil { | ||
return srv.h2Err | ||
} | ||
|
||
srv.mu.Lock() | ||
ok := !srv.closed | ||
if ok { | ||
srv.listeners[ln] = struct{}{} | ||
} | ||
srv.mu.Unlock() | ||
if !ok { | ||
return http.ErrServerClosed | ||
} | ||
|
||
defer func() { | ||
srv.mu.Lock() | ||
delete(srv.listeners, ln) | ||
srv.mu.Unlock() | ||
}() | ||
|
||
var delay time.Duration | ||
for { | ||
conn, err := ln.Accept() | ||
if ne, ok := err.(net.Error); ok && ne.Timeout() { | ||
if delay == 0 { | ||
delay = 5 * time.Millisecond | ||
} else { | ||
delay *= 2 | ||
} | ||
if max := 1 * time.Second; delay > max { | ||
delay = max | ||
} | ||
srv.errorLog().Printf("listener %q: accept error (retrying in %v): %v", ln.Addr(), delay, err) | ||
time.Sleep(delay) | ||
} else if err != nil { | ||
return fmt.Errorf("failed to accept connection: %w", err) | ||
} | ||
|
||
delay = 0 | ||
|
||
go func() { | ||
if err := srv.serveConn(conn); err != nil { | ||
srv.errorLog().Printf("listener %q: %v", ln.Addr(), err) | ||
} | ||
}() | ||
} | ||
} | ||
|
||
func (srv *Server) serveConn(conn net.Conn) error { | ||
var proto string | ||
switch conn := conn.(type) { | ||
case *tls.Conn: | ||
proto = conn.ConnectionState().NegotiatedProtocol | ||
case *proxyproto.Conn: | ||
if proxyHeader := conn.ProxyHeader(); proxyHeader != nil { | ||
tlvs, err := proxyHeader.TLVs() | ||
if err != nil { | ||
conn.Close() | ||
return err | ||
} | ||
for _, tlv := range tlvs { | ||
if tlv.Type == proxyproto.PP2_TYPE_ALPN { | ||
proto = string(tlv.Value) | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
switch proto { | ||
case "h2", "h2c": | ||
defer conn.Close() | ||
opts := http2.ServeConnOpts{ | ||
Handler: srv.h1.Handler, | ||
} | ||
srv.h2.ServeConn(conn, &opts) | ||
return nil | ||
case "", "http/1.0", "http/1.1": | ||
return srv.h1Listener.ServeConn(conn) | ||
default: | ||
conn.Close() | ||
return fmt.Errorf("unsupported protocol %q", proto) | ||
} | ||
} | ||
|
||
func (srv *Server) closeListeners() error { | ||
srv.mu.Lock() | ||
defer srv.mu.Unlock() | ||
|
||
srv.closed = true | ||
|
||
var err error | ||
for ln := range srv.listeners { | ||
if cerr := ln.Close(); cerr != nil { | ||
err = cerr | ||
} | ||
} | ||
return err | ||
} | ||
|
||
// h1Listener is used to signal back http.Server's Close and Shutdown to the | ||
// HTTP/2 server. | ||
type h1Listener struct { | ||
*pipeListener | ||
srv *Server | ||
} | ||
|
||
func (ln h1Listener) Close() error { | ||
// pipeListener.Close never fails | ||
_ = ln.pipeListener.Close() | ||
return ln.srv.closeListeners() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package http2_test | ||
|
||
import ( | ||
"errors" | ||
"log" | ||
"net" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/pires/go-proxyproto" | ||
h2proxy "github.com/pires/go-proxyproto/helper/http2" | ||
"golang.org/x/net/http2" | ||
) | ||
|
||
func ExampleServer() { | ||
ln, err := net.Listen("tcp", "localhost:80") | ||
if err != nil { | ||
log.Fatalf("failed to listen: %v", err) | ||
} | ||
|
||
proxyLn := &proxyproto.Listener{ | ||
Listener: ln, | ||
} | ||
|
||
server := h2proxy.NewServer(&http.Server{ | ||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
_, _ = w.Write([]byte("Hello world!\n")) | ||
}), | ||
}, nil) | ||
if err := server.Serve(proxyLn); err != nil { | ||
log.Fatalf("failed to serve: %v", err) | ||
} | ||
} | ||
|
||
func TestServer_h1(t *testing.T) { | ||
addr, server := newTestServer(t) | ||
defer server.Close() | ||
|
||
resp, err := http.Get("http://" + addr) | ||
if err != nil { | ||
t.Fatalf("failed to perform HTTP request: %v", err) | ||
} | ||
resp.Body.Close() | ||
} | ||
|
||
func TestServer_h2(t *testing.T) { | ||
addr, server := newTestServer(t) | ||
defer server.Close() | ||
|
||
conn, err := net.Dial("tcp", addr) | ||
if err != nil { | ||
t.Fatalf("failed to dial: %v", err) | ||
} | ||
defer conn.Close() | ||
|
||
proxyHeader := proxyproto.Header{ | ||
Version: 2, | ||
Command: proxyproto.LOCAL, | ||
TransportProtocol: proxyproto.UNSPEC, | ||
} | ||
tlvs := []proxyproto.TLV{{ | ||
Type: proxyproto.PP2_TYPE_ALPN, | ||
Value: []byte("h2"), | ||
}} | ||
if err := proxyHeader.SetTLVs(tlvs); err != nil { | ||
t.Fatalf("failed to set TLVs: %v", err) | ||
} | ||
if _, err := proxyHeader.WriteTo(conn); err != nil { | ||
t.Fatalf("failed to write PROXY header: %v", err) | ||
} | ||
|
||
h2Conn, err := new(http2.Transport).NewClientConn(conn) | ||
if err != nil { | ||
t.Fatalf("failed to create HTTP connection: %v", err) | ||
} | ||
|
||
req, err := http.NewRequest(http.MethodGet, "http://"+addr, nil) | ||
if err != nil { | ||
t.Fatalf("failed to create HTTP request: %v", err) | ||
} | ||
|
||
resp, err := h2Conn.RoundTrip(req) | ||
if err != nil { | ||
t.Fatalf("failed to perform HTTP request: %v", err) | ||
} | ||
resp.Body.Close() | ||
} | ||
|
||
func newTestServer(t *testing.T) (addr string, server *http.Server) { | ||
ln, err := net.Listen("tcp", "localhost:0") | ||
if err != nil { | ||
t.Fatalf("failed to listen: %v", err) | ||
} | ||
|
||
server = &http.Server{ | ||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
}), | ||
} | ||
|
||
h2Server := h2proxy.NewServer(server, nil) | ||
done := make(chan error, 1) | ||
go func() { | ||
done <- h2Server.Serve(&proxyproto.Listener{Listener: ln}) | ||
}() | ||
|
||
t.Cleanup(func() { | ||
err := <-done | ||
if err != nil && !errors.Is(err, net.ErrClosed) { | ||
t.Fatalf("failed to serve: %v", err) | ||
} | ||
}) | ||
|
||
return ln.Addr().String(), server | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package http2 | ||
|
||
import ( | ||
"net" | ||
"sync" | ||
) | ||
|
||
// pipeListener is a hack to workaround the lack of http.Server.ServeConn. | ||
// See: https://github.com/golang/go/issues/36673 | ||
type pipeListener struct { | ||
ch chan net.Conn | ||
closed bool | ||
mu sync.Mutex | ||
} | ||
|
||
func newPipeListener() *pipeListener { | ||
return &pipeListener{ | ||
ch: make(chan net.Conn, 64), | ||
} | ||
} | ||
|
||
func (ln *pipeListener) Accept() (net.Conn, error) { | ||
conn, ok := <-ln.ch | ||
if !ok { | ||
return nil, net.ErrClosed | ||
} | ||
return conn, nil | ||
} | ||
|
||
func (ln *pipeListener) Close() error { | ||
ln.mu.Lock() | ||
defer ln.mu.Unlock() | ||
|
||
if ln.closed { | ||
return net.ErrClosed | ||
} | ||
ln.closed = true | ||
close(ln.ch) | ||
return nil | ||
} | ||
|
||
// ServeConn enqueues a new connection. The connection will be returned in the | ||
// next Accept call. | ||
func (ln *pipeListener) ServeConn(conn net.Conn) error { | ||
ln.mu.Lock() | ||
defer ln.mu.Unlock() | ||
|
||
if ln.closed { | ||
return net.ErrClosed | ||
} | ||
ln.ch <- conn | ||
return nil | ||
} | ||
|
||
func (ln *pipeListener) Addr() net.Addr { | ||
return pipeAddr{} | ||
} | ||
|
||
type pipeAddr struct{} | ||
|
||
func (pipeAddr) Network() string { | ||
return "pipe" | ||
} | ||
|
||
func (pipeAddr) String() string { | ||
return "pipe" | ||
} |