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

Implement http3.Server.ServeListener #3349

Merged
merged 10 commits into from Mar 21, 2022
130 changes: 78 additions & 52 deletions http3/server.go
Expand Up @@ -51,6 +51,44 @@ func versionToALPN(v protocol.VersionNumber) string {
return ""
}

// ConfigureTLSConfig creates a new tls.Config which can be used
// to create a quic.Listener meant for serving http3. The created
// tls.Config adds the functionality of detecting the used QUIC version
// in order to set the correct ALPN value for the http3 connection.
func ConfigureTLSConfig(tlsConf *tls.Config) *tls.Config {
// The tls.Config used to setup the quic.Listener needs to have the GetConfigForClient callback set.
// That way, we can get the QUIC version and set the correct ALPN value.
return &tls.Config{
GetConfigForClient: func(ch *tls.ClientHelloInfo) (*tls.Config, error) {
// determine the ALPN from the QUIC version used
proto := nextProtoH3Draft29
if qconn, ok := ch.Conn.(handshake.ConnWithVersion); ok {
if qconn.GetQUICVersion() == protocol.Version1 {
proto = nextProtoH3
}
}
config := tlsConf
if tlsConf.GetConfigForClient != nil {
getConfigForClient := tlsConf.GetConfigForClient
var err error
conf, err := getConfigForClient(ch)
if err != nil {
return nil, err
}
if conf != nil {
config = conf
}
}
if config == nil {
return nil, nil
}
config = config.Clone()
config.NextProtos = []string{proto}
return config, nil
},
}
}

// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation.
type contextKey struct {
Expand Down Expand Up @@ -111,7 +149,7 @@ func (s *Server) ListenAndServe() error {
if s.Server == nil {
return errors.New("use of http3.Server without http.Server")
}
return s.serveImpl(s.TLSConfig, nil)
return s.serveConn(s.TLSConfig, nil)
}

// ListenAndServeTLS listens on the UDP address s.Addr and calls s.Handler to handle HTTP/3 requests on incoming connections.
Expand All @@ -127,17 +165,52 @@ func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
config := &tls.Config{
Certificates: certs,
}
return s.serveImpl(config, nil)
return s.serveConn(config, nil)
}

// Serve an existing UDP connection.
// It is possible to reuse the same connection for outgoing connections.
// Closing the server does not close the packet conn.
func (s *Server) Serve(conn net.PacketConn) error {
return s.serveImpl(s.TLSConfig, conn)
return s.serveConn(s.TLSConfig, conn)
}

// Serve an existing QUIC listener.
// Make sure you use http3.ConfigureTLSConfig to configure a tls.Config
// and use it to construct a http3-friendly QUIC listener.
// Closing the server does close the listener.
func (s *Server) ServeListener(listener quic.EarlyListener) error {
return s.serveImpl(func() (quic.EarlyListener, error) { return listener, nil })
}

func (s *Server) serveConn(tlsConf *tls.Config, conn net.PacketConn) error {
return s.serveImpl(func() (quic.EarlyListener, error) {
renbou marked this conversation as resolved.
Show resolved Hide resolved
baseConf := ConfigureTLSConfig(tlsConf)
quicConf := s.QuicConfig
if quicConf == nil {
quicConf = &quic.Config{}
} else {
quicConf = s.QuicConfig.Clone()
}
if s.EnableDatagrams {
quicConf.EnableDatagrams = true
}

var ln quic.EarlyListener
var err error
if conn == nil {
ln, err = quicListenAddr(s.Addr, baseConf, quicConf)
} else {
ln, err = quicListen(conn, baseConf, quicConf)
}
if err != nil {
return nil, err
}
return ln, nil
})
}

func (s *Server) serveImpl(tlsConf *tls.Config, conn net.PacketConn) error {
func (s *Server) serveImpl(startListener func() (quic.EarlyListener, error)) error {
if s.closed.Get() {
return http.ErrServerClosed
}
Expand All @@ -148,54 +221,7 @@ func (s *Server) serveImpl(tlsConf *tls.Config, conn net.PacketConn) error {
s.logger = utils.DefaultLogger.WithPrefix("server")
})

// The tls.Config we pass to Listen needs to have the GetConfigForClient callback set.
// That way, we can get the QUIC version and set the correct ALPN value.
baseConf := &tls.Config{
GetConfigForClient: func(ch *tls.ClientHelloInfo) (*tls.Config, error) {
// determine the ALPN from the QUIC version used
proto := nextProtoH3Draft29
if qconn, ok := ch.Conn.(handshake.ConnWithVersion); ok {
if qconn.GetQUICVersion() == protocol.Version1 {
proto = nextProtoH3
}
}
config := tlsConf
if tlsConf.GetConfigForClient != nil {
getConfigForClient := tlsConf.GetConfigForClient
var err error
conf, err := getConfigForClient(ch)
if err != nil {
return nil, err
}
if conf != nil {
config = conf
}
}
if config == nil {
return nil, nil
}
config = config.Clone()
config.NextProtos = []string{proto}
return config, nil
},
}

var ln quic.EarlyListener
var err error
quicConf := s.QuicConfig
if quicConf == nil {
quicConf = &quic.Config{}
} else {
quicConf = s.QuicConfig.Clone()
}
if s.EnableDatagrams {
quicConf.EnableDatagrams = true
}
if conn == nil {
ln, err = quicListenAddr(s.Addr, baseConf, quicConf)
} else {
ln, err = quicListen(conn, baseConf, quicConf)
}
ln, err := startListener()
if err != nil {
return err
}
Expand Down
117 changes: 117 additions & 0 deletions http3/server_test.go
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net"
"net/http"
"sync/atomic"
"time"

"github.com/lucas-clemente/quic-go"
Expand Down Expand Up @@ -619,6 +620,35 @@ var _ = Describe("Server", func() {
Expect(serv.ListenAndServe()).To(MatchError(http.ErrServerClosed))
})

Context("ConfigureTLSConfig", func() {
var tlsConf *tls.Config
var ch *tls.ClientHelloInfo

BeforeEach(func() {
tlsConf = &tls.Config{}
ch = &tls.ClientHelloInfo{}
})

It("advertises draft by default", func() {
tlsConf = ConfigureTLSConfig(tlsConf)
Expect(tlsConf.GetConfigForClient).NotTo(BeNil())

config, err := tlsConf.GetConfigForClient(ch)
Expect(err).NotTo(HaveOccurred())
Expect(config.NextProtos).To(Equal([]string{nextProtoH3Draft29}))
})

It("advertises h3 for quic version 1", func() {
tlsConf = ConfigureTLSConfig(tlsConf)
Expect(tlsConf.GetConfigForClient).NotTo(BeNil())

ch.Conn = newMockConn(protocol.Version1)
config, err := tlsConf.GetConfigForClient(ch)
Expect(err).NotTo(HaveOccurred())
Expect(config.NextProtos).To(Equal([]string{nextProtoH3}))
})
})

Context("Serve", func() {
origQuicListen := quicListen

Expand Down Expand Up @@ -704,6 +734,93 @@ var _ = Describe("Server", func() {
})
})

Context("ServeListener", func() {
origQuicListen := quicListen

AfterEach(func() {
quicListen = origQuicListen
})

It("serves a listener", func() {
var called int32
ln := mockquic.NewMockEarlyListener(mockCtrl)
quicListen = func(conn net.PacketConn, tlsConf *tls.Config, config *quic.Config) (quic.EarlyListener, error) {
atomic.StoreInt32(&called, 1)
return ln, nil
}

s := &Server{Server: &http.Server{}}
s.TLSConfig = &tls.Config{}

stopAccept := make(chan struct{})
ln.EXPECT().Accept(gomock.Any()).DoAndReturn(func(context.Context) (quic.Session, error) {
<-stopAccept
return nil, errors.New("closed")
})
done := make(chan struct{})
go func() {
defer GinkgoRecover()
defer close(done)
s.ServeListener(ln)
}()

Consistently(func() int32 { return atomic.LoadInt32(&called) }).Should(Equal(int32(0)))
Consistently(done).ShouldNot(BeClosed())
ln.EXPECT().Close().Do(func() { close(stopAccept) })
Expect(s.Close()).To(Succeed())
Eventually(done).Should(BeClosed())
})

It("serves two listeners", func() {
var called int32
ln1 := mockquic.NewMockEarlyListener(mockCtrl)
ln2 := mockquic.NewMockEarlyListener(mockCtrl)
lns := make(chan quic.EarlyListener, 2)
lns <- ln1
lns <- ln2
quicListen = func(c net.PacketConn, tlsConf *tls.Config, config *quic.Config) (quic.EarlyListener, error) {
atomic.StoreInt32(&called, 1)
return <-lns, nil
}

s := &Server{Server: &http.Server{}}
s.TLSConfig = &tls.Config{}

stopAccept1 := make(chan struct{})
ln1.EXPECT().Accept(gomock.Any()).DoAndReturn(func(context.Context) (quic.Session, error) {
<-stopAccept1
return nil, errors.New("closed")
})
stopAccept2 := make(chan struct{})
ln2.EXPECT().Accept(gomock.Any()).DoAndReturn(func(context.Context) (quic.Session, error) {
<-stopAccept2
return nil, errors.New("closed")
})

done1 := make(chan struct{})
go func() {
defer GinkgoRecover()
defer close(done1)
s.ServeListener(ln1)
}()
done2 := make(chan struct{})
go func() {
defer GinkgoRecover()
defer close(done2)
s.ServeListener(ln2)
}()

Consistently(func() int32 { return atomic.LoadInt32(&called) }).Should(Equal(int32(0)))
Consistently(done1).ShouldNot(BeClosed())
Expect(done2).ToNot(BeClosed())
ln1.EXPECT().Close().Do(func() { close(stopAccept1) })
ln2.EXPECT().Close().Do(func() { close(stopAccept2) })
Expect(s.Close()).To(Succeed())
Eventually(done1).Should(BeClosed())
Eventually(done2).Should(BeClosed())
})
})

Context("ListenAndServe", func() {
BeforeEach(func() {
s.Server.Addr = "localhost:0"
Expand Down