77 "fmt"
88 "io/fs"
99 "log/slog"
10+ "net"
1011 "net/http"
1112 "net/netip"
1213 "runtime"
@@ -330,6 +331,24 @@ func (web *webAPI) tlsConfigChanged(ctx context.Context) {
330331// loggerKeyServer is the key used by [webAPI] to identify servers.
331332const loggerKeyServer = "server"
332333
334+ // getBindAddr returns the network and address strings to use when creating a
335+ // listener on addr and port. network must be either "tcp" or "udp". The
336+ // address family of addr is preserved: for the unspecified IPv4 address the
337+ // IPv4-only network is returned, since Go's wildcard listeners otherwise
338+ // accept connections of both address families on platforms that support
339+ // IPv4-mapped IPv6 addresses. For the unspecified IPv6 address the returned
340+ // address is in the ":port" form, which enables dual-stack listening.
341+ func getBindAddr (network string , addr netip.Addr , port uint16 ) (listenNetwork , addrStr string ) {
342+ switch {
343+ case ! addr .IsUnspecified ():
344+ return network , netip .AddrPortFrom (addr , port ).String ()
345+ case addr .Is4 ():
346+ return network + "4" , netip .AddrPortFrom (addr , port ).String ()
347+ default :
348+ return network , netutil .JoinHostPort ("" , port )
349+ }
350+ }
351+
333352// start starts serving HTTP requests.
334353func (web * webAPI ) start (ctx context.Context ) {
335354 defer slogutil .RecoverAndExit (ctx , web .logger , osutil .ExitCodeFailure )
@@ -359,9 +378,11 @@ func (web *webAPI) start(ctx context.Context) {
359378 protocols .SetUnencryptedHTTP2 (true )
360379 protocols .SetHTTP1 (true )
361380
381+ network , addrStr := getBindAddr ("tcp" , web .conf .BindAddr .Addr (), web .conf .BindAddr .Port ())
382+
362383 // Create a new instance, because the Web is not usable after Shutdown.
363384 web .httpServer = & http.Server {
364- Addr : web . conf . BindAddr . String () ,
385+ Addr : addrStr ,
365386 Handler : hdlr ,
366387 ReadTimeout : web .conf .ReadTimeout ,
367388 ReadHeaderTimeout : web .conf .ReadHeaderTimeout ,
@@ -372,9 +393,16 @@ func (web *webAPI) start(ctx context.Context) {
372393 go func () {
373394 defer slogutil .RecoverAndLog (ctx , logger )
374395
375- logger .InfoContext (ctx , "starting plain server" , "addr" , web .httpServer .Addr )
396+ logger .InfoContext (ctx , "starting plain server" , "addr" , addrStr )
397+
398+ ln , lErr := net .Listen (network , addrStr )
399+ if lErr != nil {
400+ errs <- lErr
376401
377- errs <- web .httpServer .ListenAndServe ()
402+ return
403+ }
404+
405+ errs <- web .httpServer .Serve (ln )
378406 }()
379407
380408 err := <- errs
@@ -455,13 +483,13 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
455483 portHTTPS = config .TLS .PortHTTPS
456484 }()
457485
458- addr := netip . AddrPortFrom ( web .conf .BindAddr .Addr (), portHTTPS ). String ( )
486+ network , addrStr := getBindAddr ( "tcp" , web .conf .BindAddr .Addr (), portHTTPS )
459487 logger := web .baseLogger .With (loggerKeyServer , "https" )
460488
461489 hdlr := web .wrapMux (logger )
462490
463491 web .httpsServer .server = & http.Server {
464- Addr : addr ,
492+ Addr : addrStr ,
465493 Handler : hdlr ,
466494 TLSConfig : web .tlsManager .TLSConfig (),
467495 ReadTimeout : web .conf .ReadTimeout ,
@@ -474,11 +502,15 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
474502 printHTTPSAddresses (ctx , web .logger , extTLSConf )
475503
476504 if web .conf .serveHTTP3 {
477- go web .mustStartHTTP3 (ctx , addr )
505+ go web .mustStartHTTP3 (ctx , portHTTPS )
478506 }
479507
480508 logger .InfoContext (ctx , "starting https server" )
481- err := web .httpsServer .server .ListenAndServeTLS ("" , "" )
509+ ln , err := net .Listen (network , addrStr )
510+ if err == nil {
511+ err = web .httpsServer .server .ServeTLS (ln , "" , "" )
512+ }
513+
482514 if ! errors .Is (err , http .ErrServerClosed ) {
483515 cleanupAlways (ctx , logger , web .pidFilePath )
484516
@@ -488,35 +520,57 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
488520 return true
489521}
490522
491- // mustStartHTTP3 initializes and starts HTTP3 server.
492- func (web * webAPI ) mustStartHTTP3 (ctx context.Context , address string ) {
523+ // mustStartHTTP3 initializes and starts HTTP3 server on the configured bind
524+ // address with the given port.
525+ func (web * webAPI ) mustStartHTTP3 (ctx context.Context , port uint16 ) {
493526 defer slogutil .RecoverAndExit (ctx , web .logger , osutil .ExitCodeFailure )
494527
495528 logger := web .baseLogger .With (loggerKeyServer , "http3" )
496529 hdlr := web .wrapMux (logger )
497530
531+ network , addrStr := getBindAddr ("udp" , web .conf .BindAddr .Addr (), port )
532+
498533 web .httpsServer .server3 = & http3.Server {
499534 // TODO(a.garipov): See if there is a way to use the error log as
500535 // well as timeouts here.
501- Addr : address ,
536+ Addr : addrStr ,
502537 TLSConfig : web .tlsManager .TLSConfig (),
503538 Handler : hdlr ,
504539 }
505540
506541 web .logger .DebugContext (ctx , "starting http/3 server" )
507- err := web .httpsServer .server3 . ListenAndServe ( )
542+ err := serveHTTP3 ( ctx , logger , web .httpsServer .server3 , network , addrStr )
508543 if ! errors .Is (err , http .ErrServerClosed ) {
509544 cleanupAlways (ctx , logger , web .pidFilePath )
510545
511546 panic (fmt .Errorf ("http3: %w" , err ))
512547 }
513548}
514549
515- // startPprof launches the debug and profiling server on the provided port.
516- // baseLogger must not be nil.
517- func startPprof (baseLogger * slog.Logger , port uint16 ) {
518- addr := netip .AddrPortFrom (netutil .IPv4Localhost (), port )
550+ // serveHTTP3 listens for UDP packets on the given network and address, and
551+ // serves HTTP/3 requests on srv until it is closed. The created packet
552+ // connection is closed before returning, since [http3.Server.Serve] does not
553+ // close connections provided by the caller. logger and srv must not be nil.
554+ func serveHTTP3 (
555+ ctx context.Context ,
556+ logger * slog.Logger ,
557+ srv * http3.Server ,
558+ network string ,
559+ addrStr string ,
560+ ) (err error ) {
561+ conn , err := net .ListenPacket (network , addrStr )
562+ if err != nil {
563+ // Don't wrap the error because it's informative enough as is.
564+ return err
565+ }
566+ defer slogutil .CloseAndLog (ctx , logger , conn , slog .LevelDebug )
567+
568+ return srv .Serve (conn )
569+ }
519570
571+ // startPprof launches the debug and profiling server on the provided port on
572+ // both IPv4 and IPv6 loopback addresses. baseLogger must not be nil.
573+ func startPprof (baseLogger * slog.Logger , port uint16 ) {
520574 runtime .SetBlockProfileRate (1 )
521575 runtime .SetMutexProfileFraction (1 )
522576
@@ -526,15 +580,26 @@ func startPprof(baseLogger *slog.Logger, port uint16) {
526580 ctx := context .Background ()
527581 logger := baseLogger .With (slogutil .KeyPrefix , "pprof" )
528582
529- go func () {
530- defer slogutil .RecoverAndLog (ctx , logger )
583+ go servePprof (ctx , logger , mux , netutil .IPv4Localhost (), port )
584+ go servePprof (ctx , logger , mux , netutil .IPv6Localhost (), port )
585+ }
531586
532- logger .InfoContext (ctx , "listening" , "addr" , addr )
533- err := http .ListenAndServe (addr .String (), mux )
534- if ! errors .Is (err , http .ErrServerClosed ) {
535- logger .ErrorContext (ctx , "shutting down" , slogutil .KeyError , err )
536- }
537- }()
587+ // servePprof serves the pprof HTTP endpoints on the given host and port.
588+ func servePprof (
589+ ctx context.Context ,
590+ logger * slog.Logger ,
591+ mux * http.ServeMux ,
592+ host netip.Addr ,
593+ port uint16 ,
594+ ) {
595+ defer slogutil .RecoverAndLog (ctx , logger )
596+
597+ addrStr := netip .AddrPortFrom (host , port ).String ()
598+ logger .InfoContext (ctx , "listening" , "addr" , addrStr )
599+ err := http .ListenAndServe (addrStr , mux )
600+ if ! errors .Is (err , http .ErrServerClosed ) {
601+ logger .ErrorContext (ctx , "shutting down" , slogutil .KeyError , err )
602+ }
538603}
539604
540605// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.
0 commit comments