66 "fmt"
77 "io/fs"
88 "log/slog"
9+ "net"
910 "net/http"
1011 "net/netip"
1112 "runtime"
@@ -345,6 +346,24 @@ func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf *tlsConfigSetti
345346// loggerKeyServer is the key used by [webAPI] to identify servers.
346347const loggerKeyServer = "server"
347348
349+ // getBindAddr returns the network and address strings to use when creating a
350+ // listener on addr and port. network must be either "tcp" or "udp". The
351+ // address family of addr is preserved: for the unspecified IPv4 address the
352+ // IPv4-only network is returned, since Go's wildcard listeners otherwise
353+ // accept connections of both address families on platforms that support
354+ // IPv4-mapped IPv6 addresses. For the unspecified IPv6 address the returned
355+ // address is in the ":port" form, which enables dual-stack listening.
356+ func getBindAddr (network string , addr netip.Addr , port uint16 ) (listenNetwork , addrStr string ) {
357+ switch {
358+ case ! addr .IsUnspecified ():
359+ return network , netip .AddrPortFrom (addr , port ).String ()
360+ case addr .Is4 ():
361+ return network + "4" , netip .AddrPortFrom (addr , port ).String ()
362+ default :
363+ return network , netutil .JoinHostPort ("" , port )
364+ }
365+ }
366+
348367// start starts serving HTTP requests.
349368func (web * webAPI ) start (ctx context.Context ) {
350369 defer slogutil .RecoverAndExit (ctx , web .logger , osutil .ExitCodeFailure )
@@ -367,9 +386,11 @@ func (web *webAPI) start(ctx context.Context) {
367386 protocols .SetUnencryptedHTTP2 (true )
368387 protocols .SetHTTP1 (true )
369388
389+ network , addrStr := getBindAddr ("tcp" , web .conf .BindAddr .Addr (), web .conf .BindAddr .Port ())
390+
370391 // Create a new instance, because the Web is not usable after Shutdown.
371392 web .httpServer = & http.Server {
372- Addr : web . conf . BindAddr . String () ,
393+ Addr : addrStr ,
373394 Handler : hdlr ,
374395 ReadTimeout : web .conf .ReadTimeout ,
375396 ReadHeaderTimeout : web .conf .ReadHeaderTimeout ,
@@ -380,9 +401,16 @@ func (web *webAPI) start(ctx context.Context) {
380401 go func () {
381402 defer slogutil .RecoverAndLog (ctx , logger )
382403
383- logger .InfoContext (ctx , "starting plain server" , "addr" , web .httpServer .Addr )
404+ logger .InfoContext (ctx , "starting plain server" , "addr" , addrStr )
405+
406+ ln , lErr := net .Listen (network , addrStr )
407+ if lErr != nil {
408+ errs <- lErr
409+
410+ return
411+ }
384412
385- errs <- web .httpServer .ListenAndServe ( )
413+ errs <- web .httpServer .Serve ( ln )
386414 }()
387415
388416 err := <- errs
@@ -463,13 +491,13 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
463491 portHTTPS = config .TLS .PortHTTPS
464492 }()
465493
466- addr := netip . AddrPortFrom ( web .conf .BindAddr .Addr (), portHTTPS ). String ( )
494+ network , addrStr := getBindAddr ( "tcp" , web .conf .BindAddr .Addr (), portHTTPS )
467495 logger := web .baseLogger .With (loggerKeyServer , "https" )
468496
469497 hdlr := web .wrapMux (logger )
470498
471499 web .httpsServer .server = & http.Server {
472- Addr : addr ,
500+ Addr : addrStr ,
473501 Handler : hdlr ,
474502 // TODO(m.kazantsev): Do not create TLS config manually, but use
475503 // [aghtls.TLSConfigProvider].
@@ -488,11 +516,15 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
488516 printHTTPAddresses (ctx , web .logger , urlutil .SchemeHTTPS , web .tlsManager )
489517
490518 if web .conf .serveHTTP3 {
491- go web .mustStartHTTP3 (ctx , addr )
519+ go web .mustStartHTTP3 (ctx , portHTTPS )
492520 }
493521
494522 logger .InfoContext (ctx , "starting https server" )
495- err := web .httpsServer .server .ListenAndServeTLS ("" , "" )
523+ ln , err := net .Listen (network , addrStr )
524+ if err == nil {
525+ err = web .httpsServer .server .ServeTLS (ln , "" , "" )
526+ }
527+
496528 if ! errors .Is (err , http .ErrServerClosed ) {
497529 cleanupAlways (ctx , logger , web .pidFilePath )
498530
@@ -502,17 +534,20 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
502534 return true
503535}
504536
505- // mustStartHTTP3 initializes and starts HTTP3 server.
506- func (web * webAPI ) mustStartHTTP3 (ctx context.Context , address string ) {
537+ // mustStartHTTP3 initializes and starts HTTP3 server on the configured bind
538+ // address with the given port.
539+ func (web * webAPI ) mustStartHTTP3 (ctx context.Context , port uint16 ) {
507540 defer slogutil .RecoverAndExit (ctx , web .logger , osutil .ExitCodeFailure )
508541
509542 logger := web .baseLogger .With (loggerKeyServer , "http3" )
510543 hdlr := web .wrapMux (logger )
511544
545+ network , addrStr := getBindAddr ("udp" , web .conf .BindAddr .Addr (), port )
546+
512547 web .httpsServer .server3 = & http3.Server {
513548 // TODO(a.garipov): See if there is a way to use the error log as
514549 // well as timeouts here.
515- Addr : address ,
550+ Addr : addrStr ,
516551 // TODO(m.kazantsev): Do not create TLS config manually, but use
517552 // [aghtls.TLSConfigProvider].
518553 TLSConfig : & tls.Config {
@@ -525,18 +560,38 @@ func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {
525560 }
526561
527562 web .logger .DebugContext (ctx , "starting http/3 server" )
528- err := web .httpsServer .server3 . ListenAndServe ( )
563+ err := serveHTTP3 ( ctx , logger , web .httpsServer .server3 , network , addrStr )
529564 if ! errors .Is (err , http .ErrServerClosed ) {
530565 cleanupAlways (ctx , logger , web .pidFilePath )
531566
532567 panic (fmt .Errorf ("http3: %w" , err ))
533568 }
534569}
535570
536- // startPprof launches the debug and profiling server on the provided port.
537- func startPprof (baseLogger * slog.Logger , port uint16 ) {
538- addr := netip .AddrPortFrom (netutil .IPv4Localhost (), port )
571+ // serveHTTP3 listens for UDP packets on the given network and address, and
572+ // serves HTTP/3 requests on srv until it is closed. The created packet
573+ // connection is closed before returning, since [http3.Server.Serve] does not
574+ // close connections provided by the caller. logger and srv must not be nil.
575+ func serveHTTP3 (
576+ ctx context.Context ,
577+ logger * slog.Logger ,
578+ srv * http3.Server ,
579+ network string ,
580+ addrStr string ,
581+ ) (err error ) {
582+ conn , err := net .ListenPacket (network , addrStr )
583+ if err != nil {
584+ // Don't wrap the error because it's informative enough as is.
585+ return err
586+ }
587+ defer slogutil .CloseAndLog (ctx , logger , conn , slog .LevelDebug )
539588
589+ return srv .Serve (conn )
590+ }
591+
592+ // startPprof launches the debug and profiling server on the provided port on
593+ // both IPv4 and IPv6 loopback addresses.
594+ func startPprof (baseLogger * slog.Logger , port uint16 ) {
540595 runtime .SetBlockProfileRate (1 )
541596 runtime .SetMutexProfileFraction (1 )
542597
@@ -546,15 +601,26 @@ func startPprof(baseLogger *slog.Logger, port uint16) {
546601 ctx := context .Background ()
547602 logger := baseLogger .With (slogutil .KeyPrefix , "pprof" )
548603
549- go func () {
550- defer slogutil .RecoverAndLog (ctx , logger )
604+ go servePprof (ctx , logger , mux , netutil .IPv4Localhost (), port )
605+ go servePprof (ctx , logger , mux , netutil .IPv6Localhost (), port )
606+ }
551607
552- logger .InfoContext (ctx , "listening" , "addr" , addr )
553- err := http .ListenAndServe (addr .String (), mux )
554- if ! errors .Is (err , http .ErrServerClosed ) {
555- logger .ErrorContext (ctx , "shutting down" , slogutil .KeyError , err )
556- }
557- }()
608+ // servePprof serves the pprof HTTP endpoints on the given host and port.
609+ func servePprof (
610+ ctx context.Context ,
611+ logger * slog.Logger ,
612+ mux * http.ServeMux ,
613+ host netip.Addr ,
614+ port uint16 ,
615+ ) {
616+ defer slogutil .RecoverAndLog (ctx , logger )
617+
618+ addrStr := netip .AddrPortFrom (host , port ).String ()
619+ logger .InfoContext (ctx , "listening" , "addr" , addrStr )
620+ err := http .ListenAndServe (addrStr , mux )
621+ if ! errors .Is (err , http .ErrServerClosed ) {
622+ logger .ErrorContext (ctx , "shutting down" , slogutil .KeyError , err )
623+ }
558624}
559625
560626// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.
0 commit comments