Skip to content

Commit

Permalink
Implement http3.Server.ServeListener (quic-go#3349)
Browse files Browse the repository at this point in the history
* feat(http3): implement serving from quic.Listener

ServeListener method added to http3.Server allowing serving from an existing listener
ConfigureTLSConfig function added to http3 which should be used to create listeners meant for serving http3.

* docs(http3): add note about using ConfigureTLSConfig to ServeListener

* fix(http3): stop serving non-created listeners after Server.Close

* refactor(http3): return ErrServerClosed once server closes instead of context.Canceled

* feat(http3): close listeners from ServeListener as well

* fix(http3): fix logger not being setup during ServeListener

* test(http3): add unit tests for serving listeners

* test(http3): add tests for ConfigureTLSConfig

* test(http3): added server hotswapping integration test

* fix: race condition in listener tests
  • Loading branch information
renbou authored and nmldiegues committed Jun 6, 2022
1 parent 81de511 commit 3d5dc92
Show file tree
Hide file tree
Showing 3 changed files with 385 additions and 52 deletions.
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) {
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

0 comments on commit 3d5dc92

Please sign in to comment.