shadowsocks/shadowsocks-rust · error

empty outbound proxy chain

Error message

empty outbound proxy chain

What it means

connect_chain dials the first hop of an outbound proxy chain; it requires at least one configured hop. When the hops list is empty there is no proxy to dial, so the function returns InvalidInput before doing any I/O. This is a configuration error surfaced at connect time.

Source

Thrown at crates/shadowsocks-service/src/net/outbound/chain.rs:45

        target: &Address,
    ) -> io::Result<OutboundProxyStream>
    where
        D: TcpDialer + Sync,
    {
        connect_chain(self.hops(), dialer, target).await
    }
}

pub(crate) async fn connect_chain<D>(
    hops: &[OutboundProxyHop],
    dialer: &D,
    target: &Address,
) -> io::Result<OutboundProxyStream>
where
    D: TcpDialer + Sync,
{
    let Some(first_hop) = hops.first() else {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "empty outbound proxy chain",
        ));
    };

    trace!("dialling first outbound proxy hop {}", first_hop.addr);
    let first_tcp = dialer.dial(&first_hop.addr).await?;
    let mut stream = OutboundProxyStream::from_tcp(first_tcp)?;

    for (idx, hop) in hops.iter().enumerate() {
        // For HTTPS hops, wrap the wire layer with TLS *before* speaking
        // the application-level CONNECT verb.
        if hop.is_https() {
            stream = tls_wrap(stream, hop.tls_sni()).await?;
        }

        let next_target = hops
            .get(idx + 1)

View on GitHub (pinned to 8eb0f0a65b)

Solutions

  1. Add at least one outbound proxy hop to the configuration before starting the server/client
  2. Validate the hop list is non-empty at config-load time and fail fast with a clear message
  3. If direct (non-proxied) outbound is intended, use the direct outbound path instead of connect_chain

Example fix

// before
let hops: Vec<OutboundProxyHop> = parse_hops(cfg); // empty
connect_chain(dialer, hops, target).await?;
// after
let hops: Vec<OutboundProxyHop> = parse_hops(cfg);
assert!(!hops.is_empty(), "at least one outbound proxy hop required");
connect_chain(dialer, hops, target).await?;
Defensive patterns

Strategy: validation

Validate before calling

if outbound_hops.is_empty() {
    return Err(anyhow!("outbound proxy chain must contain at least one hop"));
}

Type guard

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

Try / catch

match connect_chain(dialer, hops, target).await {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput && e.to_string().contains("empty outbound proxy chain") => configure_fallback_hop()?,
    other => other?,
}

Prevention

When it happens

Trigger: Calling connect_tcp or connect_chain_for_udp_associate with an outbound config that resolved to a zero-length hop list (e.g. empty `outbound_forward` / proxy chain in config).

Common situations: Config file has an empty proxy chain array; the proxy section was commented out or omitted while chaining mode is still enabled; a loader filtered out all invalid proxy entries leaving none.

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/1733abe1ac012d08. Report an issue: GitHub.