@marcel91200 / caddy Mirror · read-only / commits / fcc7860d
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 09:45 UTC

reverseproxy: replace placeholders specified for sni while using http3 (#7737)

* reverseproxy: replace placeholders specified for sni while using http3 * add test for placeholder * reverseproxy: replace placeholders specified for sni while using http3 * add test for placeholder * reverseproxy: test HTTP/3 SNI host placeholder --------- Co-authored-by: Zen Dodd <mail@steadytao.com>

WeidiDeng committed Jun 3, 2026 at 11:49 UTC fcc7860d038a5cb191cf8b1410bd3ea2feeea31a
2 files changed +135 -1
caddytest/integration/reverseproxy_test.go
+38
@@ -793,3 +793,41 @@ func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
793 // Transport error on broken upstream should be retried to good upstream
794 tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
795 }
796 +
797 +func TestReverseProxySNIPlaceHolder(t *testing.T) {
798 + configTemplate := `
799 + {
800 + skip_install_trust
801 + local_certs
802 + admin localhost:2999
803 + http_port 9080
804 + https_port 9443
805 + grace_period 1ns
806 + }
807 + localhost example.com {
808 + @proxied header X-Transport caddy
809 + respond @proxied {http.request.tls.server_name}
810 + reverse_proxy 127.0.0.1:9443 {
811 + header_up X-Transport caddy
812 + header_up Host {host}
813 + transport http {
814 + versions %s
815 + tls_server_name {header.X-SNI}
816 + tls_insecure_skip_verify
817 + }
818 + }
819 + }
820 + `
821 + for _, versions := range []string{"1.1 2", "3"} {
822 + tester := caddytest.NewTester(t)
823 + tester.InitServer(fmt.Sprintf(configTemplate, versions), "caddyfile")
824 + req, err := http.NewRequest("GET", "https://localhost:9443", nil)
825 + if err != nil {
826 + t.Errorf("failed to create request %s", err)
827 + return
828 + }
829 +
830 + req.Header.Set("X-SNI", "example.com")
831 + tester.AssertResponse(req, 200, "example.com")
832 + }
833 +}
modules/caddyhttp/reverseproxy/httptransport.go
+97 -1
@@ -32,6 +32,7 @@ import (
32 "time"
33
34 "github.com/pires/go-proxyproto"
35 + "github.com/quic-go/quic-go"
36 "github.com/quic-go/quic-go/http3"
37 "go.uber.org/zap"
38 "go.uber.org/zap/zapcore"
@@ -161,7 +162,8 @@ type HTTPTransport struct {
162 // `HTTPS_PROXY`, and `NO_PROXY` environment variables.
163 NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"`
164
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
167 }
168
169 // CaddyModule returns the Caddy module information.
@@ -499,6 +501,25 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
501 if err != nil {
502 return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err)
503 }
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 + }
523 }
524 } else if len(h.Versions) > 1 && slices.Contains(h.Versions, "3") {
525 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
546 return rt, nil
547 }
548
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 +
614 // RequestHeaderOps implements TransportHeaderOpsProvider. It returns header
615 // operations for requests when the transport's configuration indicates they
616 // should be applied. In particular, when TLS is enabled for this transport,
@@ -623,6 +709,16 @@ func (h HTTPTransport) Cleanup() error {
709 return nil
710 }
711 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 + }
722 return nil
723 }
724