@@ -32,6 +32,7 @@ import (
3232 "time"
3333
3434 "github.com/pires/go-proxyproto"
35+ "github.com/quic-go/quic-go"
3536 "github.com/quic-go/quic-go/http3"
3637 "go.uber.org/zap"
3738 "go.uber.org/zap/zapcore"
@@ -161,7 +162,8 @@ type HTTPTransport struct {
161162 // `HTTPS_PROXY`, and `NO_PROXY` environment variables.
162163 NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"`
163164
164- h3Transport * http3.Transport // TODO: EXPERIMENTAL (May 2024)
165+ h3Transport * http3.Transport // TODO: EXPERIMENTAL (May 2024)
166+ quicTransport * quic.Transport // used by h3Transport if sni placeholder is used, otherwise nil
165167}
166168
167169// CaddyModule returns the Caddy module information.
@@ -499,6 +501,25 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
499501 if err != nil {
500502 return nil , fmt .Errorf ("making TLS client config for HTTP/3 transport: %v" , err )
501503 }
504+
505+ if strings .Contains (h .TLS .ServerName , "{" ) {
506+ // copied from quic-go
507+ udpConn , err := net .ListenUDP ("udp" , nil )
508+ if err != nil {
509+ return nil , fmt .Errorf ("making udp socket for HTTP/3 transport: %v" , err )
510+ }
511+ h .quicTransport = & quic.Transport {Conn : udpConn }
512+ h .h3Transport .Dial = func (ctx context.Context , addr string , tlsCfg * tls.Config , cfg * quic.Config ) (* quic.Conn , error ) {
513+ // tlsCfg is already cloned from h3Transport.TLSClientConfig
514+ repl := ctx .Value (caddy .ReplacerCtxKey ).(* caddy.Replacer )
515+ tlsCfg .ServerName = repl .ReplaceAll (tlsCfg .ServerName , "" )
516+ udpAddr , err := resolveUDPAddr (ctx , "udp" , addr )
517+ if err != nil {
518+ return nil , err
519+ }
520+ return h .quicTransport .DialEarly (ctx , udpAddr , tlsCfg , cfg )
521+ }
522+ }
502523 }
503524 } else if len (h .Versions ) > 1 && slices .Contains (h .Versions , "3" ) {
504525 return nil , fmt .Errorf ("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported" )
@@ -525,6 +546,71 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
525546 return rt , nil
526547}
527548
549+ // TODO: EXPERIMENTAL (May 2025)
550+ // copied from quic-go
551+ func resolveUDPAddr (ctx context.Context , network , addr string ) (* net.UDPAddr , error ) {
552+ host , portStr , err := net .SplitHostPort (addr )
553+ if err != nil {
554+ return nil , err
555+ }
556+ port , err := net .LookupPort (network , portStr )
557+ if err != nil {
558+ return nil , err
559+ }
560+ resolver := net .DefaultResolver
561+ ipAddrs , err := resolver .LookupIPAddr (ctx , host )
562+ if err != nil {
563+ return nil , err
564+ }
565+ addrs := addrList (ipAddrs )
566+ ip := addrs .forResolve (network , addr )
567+ return & net.UDPAddr {IP : ip .IP , Port : port , Zone : ip .Zone }, nil
568+ }
569+
570+ // TODO: EXPERIMENTAL (May 2025)
571+ // copied from quic-go
572+ // An addrList represents a list of network endpoint addresses.
573+ // Copy from [net.addrList] and change type from [net.Addr] to [net.IPAddr]
574+ type addrList []net.IPAddr
575+
576+ // isIPv4 reports whether addr contains an IPv4 address.
577+ func isIPv4 (addr net.IPAddr ) bool {
578+ return addr .IP .To4 () != nil
579+ }
580+
581+ // isNotIPv4 reports whether addr does not contain an IPv4 address.
582+ func isNotIPv4 (addr net.IPAddr ) bool { return ! isIPv4 (addr ) }
583+
584+ // forResolve returns the most appropriate address in address for
585+ // a call to ResolveTCPAddr, ResolveUDPAddr, or ResolveIPAddr.
586+ // IPv4 is preferred, unless addr contains an IPv6 literal.
587+ func (addrs addrList ) forResolve (network , addr string ) net.IPAddr {
588+ var want6 bool
589+ switch network {
590+ case "ip" :
591+ // IPv6 literal (addr does NOT contain a port)
592+ want6 = strings .ContainsRune (addr , ':' )
593+ case "tcp" , "udp" :
594+ // IPv6 literal. (addr contains a port, so look for '[')
595+ want6 = strings .ContainsRune (addr , '[' )
596+ }
597+ if want6 {
598+ return addrs .first (isNotIPv4 )
599+ }
600+ return addrs .first (isIPv4 )
601+ }
602+
603+ // first returns the first address which satisfies strategy, or if
604+ // none do, then the first address of any kind.
605+ func (addrs addrList ) first (strategy func (net.IPAddr ) bool ) net.IPAddr {
606+ for _ , addr := range addrs {
607+ if strategy (addr ) {
608+ return addr
609+ }
610+ }
611+ return addrs [0 ]
612+ }
613+
528614// RequestHeaderOps implements TransportHeaderOpsProvider. It returns header
529615// operations for requests when the transport's configuration indicates they
530616// should be applied. In particular, when TLS is enabled for this transport,
@@ -623,6 +709,16 @@ func (h HTTPTransport) Cleanup() error {
623709 return nil
624710 }
625711 h .Transport .CloseIdleConnections ()
712+ // h3 related cleanup, errors are ignored as nothing can be done.
713+ // TODO: log these errors if any
714+ if h .h3Transport != nil {
715+ h .h3Transport .CloseIdleConnections ()
716+ _ = h .h3Transport .Close ()
717+ if h .quicTransport != nil {
718+ _ = h .quicTransport .Close ()
719+ _ = h .quicTransport .Conn .Close ()
720+ }
721+ }
626722 return nil
627723}
628724
0 commit comments