shadowsocks/shadowsocks-rust · error

empty outbound proxy chain

Error message

empty outbound proxy chain

What it means

OutboundUdpProxy::associate builds a UDP relay over the proxy chain, but UDP relaying is only possible when at least one hop is configured and (per the next check) all hops are SOCKS5. An empty hop list yields InvalidInput with this message before any networking occurs.

Source

Thrown at crates/shadowsocks-service/src/net/outbound/udp.rs:104

    /// Establish UDP relays through every hop of `client`.
    ///
    /// * `target` is the inner-most destination (typically the ss-server's
    ///   UDP external address).
    /// * `dialer` is used to dial all TCP control connections.
    /// * `connect_opts` configures the local UDP socket.
    pub async fn associate<D>(
        client: &OutboundProxyClient,
        context: &SharedContext,
        dialer: &D,
        connect_opts: &ConnectOpts,
        target: Address,
    ) -> io::Result<Self>
    where
        D: TcpDialer + Sync,
    {
        let hops = client.hops();
        if hops.is_empty() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "empty outbound proxy chain",
            ));
        }
        for hop in hops {
            if !matches!(hop.kind, OutboundProxyKind::Socks5 { .. }) {
                return Err(io::Error::new(
                    io::ErrorKind::Unsupported,
                    "outbound UDP relay requires every hop to be SOCKS5",
                ));
            }
        }

        // Bind the local UDP socket using the shadowsocks helper so
        // `ConnectOpts` (bind address, fwmark, ...) is honoured.
        let socket = ShadowUdpSocket::connect_any_with_opts(AddrFamily::Ipv4, connect_opts).await?;

        let local_udp_addr = socket.local_addr()?;

View on GitHub (pinned to 8eb0f0a65b)

Solutions

  1. Configure at least one SOCKS5 proxy hop before creating the UDP associate
  2. Validate hops().is_empty() at config load and reject the configuration early
  3. Use the direct UDP socket path if no proxy is intended

Example fix

// before
if client.hops().is_empty() { return Err(...); } // hit at runtime
// after
// at config load:
ensure!(!cfg.udp_hops.is_empty(), "udp outbound requires at least one socks5 hop");
Defensive patterns

Strategy: validation

Validate before calling

if client.hops().is_empty() {
    return Err(anyhow!("UDP outbound needs at least one socks5 hop"));
}

Type guard

fn udp_capable(hops: &[OutboundProxyHop]) -> bool { !hops.is_empty() }

Try / catch

match OutboundUdpProxy::associate(client, peer).await {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput && e.to_string().contains("empty outbound proxy chain") => configure_udp_hops()?,
    other => other?,
}

Prevention

When it happens

Trigger: Calling OutboundUdpProxy::associate with a client whose hops() vector is empty — the same misconfiguration as error 100 but on the UDP path.

Common situations: Empty or missing UDP forward-proxy config; all proxy entries rejected at parse time; the user expects direct UDP forwarding but supplied no proxies.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of shadowsocks/shadowsocks-rust@8eb0f0a65b (2026-09-09). Data as JSON: /api/errors/0593709c36cbb153. Report an issue: GitHub.