Skip to content

Commit fcc7860

Browse files
WeidiDengsteadytao
andauthored
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>
1 parent e2eee6a commit fcc7860

2 files changed

Lines changed: 135 additions & 1 deletion

File tree

caddytest/integration/reverseproxy_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,3 +793,41 @@ func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
793793
// Transport error on broken upstream should be retried to good upstream
794794
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
795795
}
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

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)