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
- Make every hop in the UDP chain OutboundProxyKind::Socks5
- Use a separate TCP-only chain for non-SOCKS5 proxies and a SOCKS5-only chain for UDP
- 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
- Keep separate chain configs for TCP and UDP outbound
- Validate every hop kind before enabling UDP relay
- Document that UDP relay requires end-to-end SOCKS5 support
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 theView on GitHub (pinned to 8eb0f0a65b)