Skip to content

Commit

Permalink
advertise multiple listeners via Alt-Svc and improve perf of SetQuicH…
Browse files Browse the repository at this point in the history
…eaders (#3352)

* feat: cache alt-svc headers and announce all listeners instead of just one

* feat: use Server.Addr for SetQuicHeaders if no port is available from listeners
  • Loading branch information
renbou committed Mar 22, 2022
1 parent 4725dde commit d4293fb
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 68 deletions.
140 changes: 104 additions & 36 deletions http3/server.go
Expand Up @@ -12,7 +12,6 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/lucas-clemente/quic-go"
Expand Down Expand Up @@ -117,6 +116,11 @@ func newConnError(code errorCode, err error) requestError {
return requestError{err: err, connErr: code}
}

// listenerInfo contains info about specific listener added with addListener
type listenerInfo struct {
port int // 0 means that no info about port is available
}

// Server is a HTTP/3 server.
type Server struct {
*http.Server
Expand All @@ -134,12 +138,14 @@ type Server struct {
// If needed Port can be manually set when the Server is created.
// This is useful when a Layer 4 firewall is redirecting UDP traffic and clients must use
// a port different from the port the Server is listening on.
Port uint32
Port int

mutex sync.Mutex
listeners map[*quic.EarlyListener]struct{}
mutex sync.RWMutex
listeners map[*quic.EarlyListener]listenerInfo
closed utils.AtomicBool

altSvcHeader string

loggerOnce sync.Once
logger utils.Logger
}
Expand Down Expand Up @@ -237,21 +243,94 @@ func (s *Server) serveImpl(startListener func() (quic.EarlyListener, error)) err
}
}

func extractPort(addr string) (int, error) {
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
return 0, err
}

portInt, err := net.LookupPort("tcp", portStr)
if err != nil {
return 0, err
}
return portInt, nil
}

func (s *Server) generateAltSvcHeader() {
if len(s.listeners) == 0 {
// Don't announce any ports since no one is listening for connections
s.altSvcHeader = ""
return
}

// This code assumes that we will use protocol.SupportedVersions if no quic.Config is passed.
supportedVersions := protocol.SupportedVersions
if s.QuicConfig != nil && len(s.QuicConfig.Versions) > 0 {
supportedVersions = s.QuicConfig.Versions
}
var versionStrings []string
for _, version := range supportedVersions {
if v := versionToALPN(version); len(v) > 0 {
versionStrings = append(versionStrings, v)
}
}

var altSvc []string
addPort := func(port int) {
for _, v := range versionStrings {
altSvc = append(altSvc, fmt.Sprintf(`%s=":%d"; ma=2592000`, v, port))
}
}

if s.Port != 0 {
// if Port is specified, we must use it instead of the
// listener addresses since there's a reason it's specified.
addPort(s.Port)
} else {
// if we have some listeners assigned, try to find ports
// which we can announce, otherwise nothing should be announced
validPortsFound := false
for _, info := range s.listeners {
if info.port != 0 {
addPort(info.port)
validPortsFound = true
}
}
if !validPortsFound {
if port, err := extractPort(s.Addr); err == nil {
addPort(port)
}
}
}

s.altSvcHeader = strings.Join(altSvc, ",")
}

// We store a pointer to interface in the map set. This is safe because we only
// call trackListener via Serve and can track+defer untrack the same pointer to
// local variable there. We never need to compare a Listener from another caller.
func (s *Server) addListener(l *quic.EarlyListener) {
s.mutex.Lock()
if s.listeners == nil {
s.listeners = make(map[*quic.EarlyListener]struct{})
s.listeners = make(map[*quic.EarlyListener]listenerInfo)
}
s.listeners[l] = struct{}{}

if port, err := extractPort((*l).Addr().String()); err == nil {
s.listeners[l] = listenerInfo{port}
} else {
s.logger.Errorf(
"Unable to extract port from listener %+v, will not be announced using SetQuicHeaders: %s", err)
s.listeners[l] = listenerInfo{}
}
s.generateAltSvcHeader()

s.mutex.Unlock()
}

func (s *Server) removeListener(l *quic.EarlyListener) {
s.mutex.Lock()
delete(s.listeners, l)
s.generateAltSvcHeader()
s.mutex.Unlock()
}

Expand Down Expand Up @@ -462,39 +541,28 @@ func (s *Server) CloseGracefully(timeout time.Duration) error {
return nil
}

// SetQuicHeaders can be used to set the proper headers that announce that this server supports QUIC.
// The values that are set depend on the port information from s.Server.Addr, and currently look like this (if Addr has port 443):
// Alt-Svc: quic=":443"; ma=2592000; v="33,32,31,30"
// ErrNoAltSvcPort is the error returned by SetQuicHeaders when no port was found
// for Alt-Svc to announce. This can happen if listening on a PacketConn without a port
// (UNIX socket, for example) and no port is specified in Server.Port or Server.Addr.
var ErrNoAltSvcPort = errors.New("no port can be announced, specify it explicitly using Server.Port or Server.Addr")

// SetQuicHeaders can be used to set the proper headers that announce that this server supports HTTP/3.
// The values set by default advertise all of the ports the server is listening on, but can be
// changed to a specific port by setting Server.Port before launching the serverr.
// If no listener's Addr().String() returns an address with a valid port, Server.Addr will be used
// to extract the port, if specified.
// For example, a server launched using ListenAndServe on an address with port 443 would set:
// Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
func (s *Server) SetQuicHeaders(hdr http.Header) error {
port := atomic.LoadUint32(&s.Port)
s.mutex.RLock()
defer s.mutex.RUnlock()

if port == 0 {
// Extract port from s.Server.Addr
_, portStr, err := net.SplitHostPort(s.Server.Addr)
if err != nil {
return err
}
portInt, err := net.LookupPort("tcp", portStr)
if err != nil {
return err
}
port = uint32(portInt)
atomic.StoreUint32(&s.Port, port)
}

// This code assumes that we will use protocol.SupportedVersions if no quic.Config is passed.
supportedVersions := protocol.SupportedVersions
if s.QuicConfig != nil && len(s.QuicConfig.Versions) > 0 {
supportedVersions = s.QuicConfig.Versions
}
altSvc := make([]string, 0, len(supportedVersions))
for _, version := range supportedVersions {
v := versionToALPN(version)
if len(v) > 0 {
altSvc = append(altSvc, fmt.Sprintf(`%s=":%d"; ma=2592000`, v, port))
}
if s.altSvcHeader == "" {
return ErrNoAltSvcPort
}
hdr.Add("Alt-Svc", strings.Join(altSvc, ","))
// use the map directly to avoid constant canonicalization
// since the key is already canonicalized
hdr["Alt-Svc"] = append(hdr["Alt-Svc"], s.altSvcHeader)
return nil
}

Expand Down

0 comments on commit d4293fb

Please sign in to comment.