66 "fmt"
77 "io/fs"
88 "log/slog"
9+ "net"
910 "net/http"
1011 "net/netip"
1112 "runtime"
@@ -334,6 +335,24 @@ func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf *aghtls.Extende
334335// loggerKeyServer is the key used by [webAPI] to identify servers.
335336const loggerKeyServer = "server"
336337
338+ // getBindAddr returns the network and address strings to use when creating a
339+ // listener on addr and port. network must be either "tcp" or "udp". The
340+ // address family of addr is preserved: for the unspecified IPv4 address the
341+ // IPv4-only network is returned, since Go's wildcard listeners otherwise
342+ // accept connections of both address families on platforms that support
343+ // IPv4-mapped IPv6 addresses. For the unspecified IPv6 address the returned
344+ // address is in the ":port" form, which enables dual-stack listening.
345+ func getBindAddr (network string , addr netip.Addr , port uint16 ) (listenNetwork , addrStr string ) {
346+ switch {
347+ case ! addr .IsUnspecified ():
348+ return network , netip .AddrPortFrom (addr , port ).String ()
349+ case addr .Is4 ():
350+ return network + "4" , netip .AddrPortFrom (addr , port ).String ()
351+ default :
352+ return network , netutil .JoinHostPort ("" , port )
353+ }
354+ }
355+
337356// start starts serving HTTP requests.
338357func (web * webAPI ) start (ctx context.Context ) {
339358 defer slogutil .RecoverAndExit (ctx , web .logger , osutil .ExitCodeFailure )
@@ -356,9 +375,11 @@ func (web *webAPI) start(ctx context.Context) {
356375 protocols .SetUnencryptedHTTP2 (true )
357376 protocols .SetHTTP1 (true )
358377
378+ network , addrStr := getBindAddr ("tcp" , web .conf .BindAddr .Addr (), web .conf .BindAddr .Port ())
379+
359380 // Create a new instance, because the Web is not usable after Shutdown.
360381 web .httpServer = & http.Server {
361- Addr : web . conf . BindAddr . String () ,
382+ Addr : addrStr ,
362383 Handler : hdlr ,
363384 ReadTimeout : web .conf .ReadTimeout ,
364385 ReadHeaderTimeout : web .conf .ReadHeaderTimeout ,
@@ -369,9 +390,16 @@ func (web *webAPI) start(ctx context.Context) {
369390 go func () {
370391 defer slogutil .RecoverAndLog (ctx , logger )
371392
372- logger .InfoContext (ctx , "starting plain server" , "addr" , web .httpServer .Addr )
393+ logger .InfoContext (ctx , "starting plain server" , "addr" , addrStr )
394+
395+ ln , lErr := net .Listen (network , addrStr )
396+ if lErr != nil {
397+ errs <- lErr
373398
374- errs <- web .httpServer .ListenAndServe ()
399+ return
400+ }
401+
402+ errs <- web .httpServer .Serve (ln )
375403 }()
376404
377405 err := <- errs
@@ -452,13 +480,13 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
452480 portHTTPS = config .TLS .PortHTTPS
453481 }()
454482
455- addr := netip . AddrPortFrom ( web .conf .BindAddr .Addr (), portHTTPS ). String ( )
483+ network , addrStr := getBindAddr ( "tcp" , web .conf .BindAddr .Addr (), portHTTPS )
456484 logger := web .baseLogger .With (loggerKeyServer , "https" )
457485
458486 hdlr := web .wrapMux (logger )
459487
460488 web .httpsServer .server = & http.Server {
461- Addr : addr ,
489+ Addr : addrStr ,
462490 Handler : hdlr ,
463491 TLSConfig : web .tlsConfProvider .TLSConfig (),
464492 ReadTimeout : web .conf .ReadTimeout ,
@@ -471,11 +499,15 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
471499 printHTTPSAddresses (ctx , web .logger , extTLSConf )
472500
473501 if web .conf .serveHTTP3 {
474- go web .mustStartHTTP3 (ctx , addr )
502+ go web .mustStartHTTP3 (ctx , portHTTPS )
475503 }
476504
477505 logger .InfoContext (ctx , "starting https server" )
478- err := web .httpsServer .server .ListenAndServeTLS ("" , "" )
506+ ln , err := net .Listen (network , addrStr )
507+ if err == nil {
508+ err = web .httpsServer .server .ServeTLS (ln , "" , "" )
509+ }
510+
479511 if ! errors .Is (err , http .ErrServerClosed ) {
480512 cleanupAlways (ctx , logger , web .pidFilePath )
481513
@@ -485,34 +517,57 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
485517 return true
486518}
487519
488- // mustStartHTTP3 initializes and starts HTTP3 server.
489- func (web * webAPI ) mustStartHTTP3 (ctx context.Context , address string ) {
520+ // mustStartHTTP3 initializes and starts HTTP3 server on the configured bind
521+ // address with the given port.
522+ func (web * webAPI ) mustStartHTTP3 (ctx context.Context , port uint16 ) {
490523 defer slogutil .RecoverAndExit (ctx , web .logger , osutil .ExitCodeFailure )
491524
492525 logger := web .baseLogger .With (loggerKeyServer , "http3" )
493526 hdlr := web .wrapMux (logger )
494527
528+ network , addrStr := getBindAddr ("udp" , web .conf .BindAddr .Addr (), port )
529+
495530 web .httpsServer .server3 = & http3.Server {
496531 // TODO(a.garipov): See if there is a way to use the error log as
497532 // well as timeouts here.
498- Addr : address ,
533+ Addr : addrStr ,
499534 TLSConfig : web .tlsConfProvider .TLSConfig (),
500535 Handler : hdlr ,
501536 }
502537
503538 web .logger .DebugContext (ctx , "starting http/3 server" )
504- err := web .httpsServer .server3 . ListenAndServe ( )
539+ err := serveHTTP3 ( ctx , logger , web .httpsServer .server3 , network , addrStr )
505540 if ! errors .Is (err , http .ErrServerClosed ) {
506541 cleanupAlways (ctx , logger , web .pidFilePath )
507542
508543 panic (fmt .Errorf ("http3: %w" , err ))
509544 }
510545}
511546
512- // startPprof launches the debug and profiling server on the provided port.
513- func startPprof (baseLogger * slog.Logger , port uint16 ) {
514- addr := netip .AddrPortFrom (netutil .IPv4Localhost (), port )
547+ // serveHTTP3 listens for UDP packets on the given network and address, and
548+ // serves HTTP/3 requests on srv until it is closed. The created packet
549+ // connection is closed before returning, since [http3.Server.Serve] does not
550+ // close connections provided by the caller. logger and srv must not be nil.
551+ func serveHTTP3 (
552+ ctx context.Context ,
553+ logger * slog.Logger ,
554+ srv * http3.Server ,
555+ network string ,
556+ addrStr string ,
557+ ) (err error ) {
558+ conn , err := net .ListenPacket (network , addrStr )
559+ if err != nil {
560+ // Don't wrap the error because it's informative enough as is.
561+ return err
562+ }
563+ defer slogutil .CloseAndLog (ctx , logger , conn , slog .LevelDebug )
564+
565+ return srv .Serve (conn )
566+ }
515567
568+ // startPprof launches the debug and profiling server on the provided port on
569+ // both IPv4 and IPv6 loopback addresses.
570+ func startPprof (baseLogger * slog.Logger , port uint16 ) {
516571 runtime .SetBlockProfileRate (1 )
517572 runtime .SetMutexProfileFraction (1 )
518573
@@ -522,15 +577,26 @@ func startPprof(baseLogger *slog.Logger, port uint16) {
522577 ctx := context .Background ()
523578 logger := baseLogger .With (slogutil .KeyPrefix , "pprof" )
524579
525- go func () {
526- defer slogutil .RecoverAndLog (ctx , logger )
580+ go servePprof (ctx , logger , mux , netutil .IPv4Localhost (), port )
581+ go servePprof (ctx , logger , mux , netutil .IPv6Localhost (), port )
582+ }
527583
528- logger .InfoContext (ctx , "listening" , "addr" , addr )
529- err := http .ListenAndServe (addr .String (), mux )
530- if ! errors .Is (err , http .ErrServerClosed ) {
531- logger .ErrorContext (ctx , "shutting down" , slogutil .KeyError , err )
532- }
533- }()
584+ // servePprof serves the pprof HTTP endpoints on the given host and port.
585+ func servePprof (
586+ ctx context.Context ,
587+ logger * slog.Logger ,
588+ mux * http.ServeMux ,
589+ host netip.Addr ,
590+ port uint16 ,
591+ ) {
592+ defer slogutil .RecoverAndLog (ctx , logger )
593+
594+ addrStr := netip .AddrPortFrom (host , port ).String ()
595+ logger .InfoContext (ctx , "listening" , "addr" , addrStr )
596+ err := http .ListenAndServe (addrStr , mux )
597+ if ! errors .Is (err , http .ErrServerClosed ) {
598+ logger .ErrorContext (ctx , "shutting down" , slogutil .KeyError , err )
599+ }
534600}
535601
536602// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.
0 commit comments