XTLS/Xray-core · critical

brutal

Error message

brutal

What it means

The splithttp dialer panicked with the raw congestion-controller name as the panic value while setting up a QUIC (HTTP/3) connection. The switch only accepts "reno", ""/"bbr", and "force-brutal"; any other string in the transport config's congestion field hits default: panic(quicParams.Congestion). The panic message 'brutal' appears when the configured value is literally the string 'brutal' — a near-miss of the accepted 'force-brutal'.

Source

Thrown at transport/internet/splithttp/dialer.go:271

						return nil, errors.New("mask err").Base(err)
					}
					pktConn = newConn
				}

				conn, err := quic.DialEarly(ctx, pktConn, udpAddr, tlsCfg, cfg)
				if err != nil {
					return nil, err
				}
				context.AfterFunc(conn.Context(), func() { pktConn.Close() })

				switch quicParams.Congestion {
				case "reno":
				case "", "bbr":
					congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
				case "force-brutal":
					congestion.UseBrutal(conn, quicParams.BrutalUp)
				default:
					panic(quicParams.Congestion)
				}

				return conn, nil
			},
		}
	} else if httpVersion == "2" {
		if keepAlivePeriod == 0 {
			keepAlivePeriod = net.ChromeH2KeepAlivePeriod
		}
		if keepAlivePeriod < 0 {
			keepAlivePeriod = 0
		}
		transport = &http2.Transport{
			DialTLSContext: func(ctxInner context.Context, network string, addr string, cfg *gotls.Config) (net.Conn, error) {
				return dialContext(ctxInner)
			},
			IdleConnTimeout: net.ConnIdleTimeout,
			ReadIdleTimeout: keepAlivePeriod,

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Change the congestion setting to an accepted value: "force-brutal" (with brutalUp bandwidth set), "bbr" or "" (default), or "reno".
  2. If you wanted the brutal (TCP-like fixed-rate) controller, use exactly "force-brutal" and configure its BrutalUp mbps value.
  3. Validate config at load time instead of panicking at dial time (see validationCode).

Example fix

// before (config.json)
"congestion": "brutal"
// after
"congestion": "force-brutal"
Defensive patterns

Strategy: validation

Validate before calling

validCongestion := map[string]bool{"": true, "reno": true, "bbr": true, "force-brutal": true}
if !validCongestion[quicParams.Congestion] {
    return fmt.Errorf("invalid congestion %q: use \"reno\", \"bbr\" or \"force-brutal\"", quicParams.Congestion)
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        return nil, fmt.Errorf("splithttp dial failed (congestion=%q): %v", quicParams.Congestion, r)
    }
}()

Prevention

When it happens

Trigger: Configuring splithttp QUIC congestion to any value outside {"reno", "", "bbr", "force-brutal"} — most commonly typing "brutal" instead of "force-brutal", or "cubic"/"newreno" from other tools' vocabularies. Panics at dial time, so the outbound connection crashes the process or goroutine depending on recover() coverage.

Common situations: Copy-pasting configs from other proxies whose brutal mode is spelled 'brutal'; upgrading configs written for a build that accepted more names; typo in JSON/YAML config.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/666d946cce8e6b70. Report an issue: GitHub.