shadowsocks/shadowsocks-rust · error

outbound UDP relay requires every hop to be SOCKS5

Error message

outbound UDP relay requires every hop to be SOCKS5

What it means

UDP relay through the outbound chain is implemented only for SOCKS5 (via UDP ASSOCIATE). If any hop in the chain is a different kind (HTTP, HTTPS, shadowsocks, etc.), associate rejects the whole chain with Unsupported since intermediate protocols cannot carry UDP.

Solutions

  1. Make every hop in the UDP chain OutboundProxyKind::Socks5
  2. Use a separate TCP-only chain for non-SOCKS5 proxies and a SOCKS5-only chain for UDP
  3. Drop the non-SOCKS5 hop or terminate UDP locally instead of relaying

Example fix

// before
hops: vec![http_hop, socks5_hop] // used for UDP
// after
hops: vec![socks5_hop_1, socks5_hop_2] // all socks5 for UDP relay
Defensive patterns

Strategy: validation

Validate before calling

fn all_socks5(hops: &[OutboundProxyHop]) -> bool {
    !hops.is_empty() && hops.iter().all(|h| matches!(h.kind, OutboundProxyKind::Socks5{..}))
}
if !all_socks5(&hops) { return Err(anyhow!("udp relay chain must be all socks5")); }

Type guard

fn is_socks5(k: &OutboundProxyKind) -> bool { matches!(k, OutboundProxyKind::Socks5{..}) }

Try / catch

match associate(client).await {
    Err(e) if e.kind() == io::ErrorKind::Unsupported && e.to_string().contains("SOCKS5") => {
        eprintln!("use socks5-only chain for udp");
    }
    other => other?,
}

Prevention

When it happens

Trigger: OutboundUdpProxy::associate receives a hop list where at least one hop is not OutboundProxyKind::Socks5 — e.g. an http hop in the middle of the chain, or a pure-https chain used for UDP.

Common situations: Sharing one proxy chain config between TCP and UDP paths; assuming HTTP CONNECT proxies forward UDP; adding an https hop for encryption without realizing UDP needs SOCKS5.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

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

        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()?;
        trace!("outbound udp local socket bound to {}", local_udp_addr);

        let mut relays: Vec<Socks5UdpRelay> = Vec::with_capacity(hops.len());

        // The "announce" address tells the next hop the source it should
        // expect datagrams from. For the first hop this is the local UDP
        // socket; for later hops it is the relay address granted by the

View on GitHub (pinned to 8eb0f0a65b)