caddyserver/caddy · error

making udp socket for HTTP/3 transport: %v

Error message

making udp socket for HTTP/3 transport: %v

What it means

Thrown while provisioning a reverse_proxy transport with HTTP/3 enabled and a placeholder in tls_server_name. Because the QUIC connection must re-resolve the SNI per request, Caddy builds a dedicated quic.Transport backed by a local UDP socket; if the OS refuses to open that socket (net.ListenUDP fails), provisioning of the whole transport aborts with this wrapped error. It is an environment/resource failure, not a config-syntax error.

Source

Thrown at modules/caddyhttp/reverseproxy/httptransport.go:509

	// configure HTTP/3 transport if enabled; however, this does not
	// automatically fall back to lower versions like most web browsers
	// do (that'd add latency and complexity, besides, we expect that
	// site owners  control the backends), so it must be exclusive
	if len(h.Versions) == 1 && h.Versions[0] == "3" {
		h.h3Transport = new(http3.Transport)
		if h.TLS != nil {
			var err error
			h.h3Transport.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(caddyCtx)
			if err != nil {
				return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err)
			}

			if strings.Contains(h.TLS.ServerName, "{") {
				// copied from quic-go
				udpConn, err := net.ListenUDP("udp", nil)
				if err != nil {
					return nil, fmt.Errorf("making udp socket for HTTP/3 transport: %v", err)
				}
				h.quicTransport = &quic.Transport{Conn: udpConn}
				h.h3Transport.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
					// tlsCfg is already cloned from h3Transport.TLSClientConfig
					repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
					tlsCfg.ServerName = repl.ReplaceAll(tlsCfg.ServerName, "")
					udpAddr, err := resolveUDPAddr(ctx, "udp", addr)
					if err != nil {
						return nil, err
					}
					return h.quicTransport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
				}
			}
		}
	} else if len(h.Versions) > 1 && slices.Contains(h.Versions, "3") {
		return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported")
	}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Check that the runtime environment permits UDP sockets: run a minimal test (e.g. a tiny Go program calling net.ListenUDP) under the same container/user; loosen the seccomp/AppArmor/SELinux rule that blocks it.
  2. If you do not need per-request dynamic SNI, remove the placeholder from tls_server_name (use a literal value) so the shared quic-go dialer is used and no extra UDP socket is created.
  3. If dynamic SNI is not required at all, drop versions 3 from transport http and proxy over HTTP/1.1 or H2 until the environment supports QUIC.
  4. Raise ephemeral port range / UDP memlock limits (sysctl net.ipv4.ip_local_port_range, rlimit memlock) if the failure is resource exhaustion.

Example fix

// before (Caddyfile)
reverse_proxy localhost:443 {
    transport http {
        versions 3
        tls
        tls_server_name {http.request.host}
    }
}

// after
reverse_proxy localhost:443 {
    transport http {
        versions 3
        tls
        tls_server_name example.internal   // literal SNI: no extra UDP socket needed
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: confirm the process can open a UDP socket before loading H3 config
conn, err := net.ListenUDP("udp", nil)
if err != nil {
    log.Fatalf("HTTP/3 upstream requires UDP sockets: %v", err)
}
conn.Close()

Try / catch

// In code embedding Caddy, treat config-load errors as transient only for this case
err := caddy.Load(cfg, false)
if err != nil && strings.Contains(err.Error(), "making udp socket for HTTP/3 transport") {
    // environment-level fix required (seccomp/limits); retry only after remediation
}
if err != nil {
    return err
}

Prevention

When it happens

Trigger: reverse_proxy with transport http { versions 3 } (or h3) plus a tls block whose server_name contains a '{...}' placeholder. The code path at httptransport.go calls net.ListenUDP("udp", nil), which fails when the process lacks permission to create UDP sockets, the UDP port space is exhausted, or a container/seccomp policy blocks socket(2)/bind(2) on UDP.

Common situations: Running Caddy in a hardened container (gVisor, restricted seccomp profile, no-network k8s sandbox) with HTTP/3 upstreams and dynamic SNI placeholders; UDP buffers or ephemeral ports exhausted; SELinux/AppArmor denying datagram sockets.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/177bad6fa1d47e9e. Report an issue: GitHub.