shadowsocks/shadowsocks-rust · error

HTTPS outbound proxy requires either `local-http-native-tls`

Error message

HTTPS outbound proxy requires either `local-http-native-tls` or `local-http-rustls` feature

What it means

HTTPS outbound proxy hops need a TLS backend; tls_unsupported is returned by tls_connect when the crate is built with neither `local-http-native-tls` nor `local-http-rustls`. The TLS layer is intentionally stubbed out so HTTPS hops cannot be established.

Source

Thrown at crates/shadowsocks-service/src/net/outbound/tls.rs:30

#[cfg(any(feature = "local-http-native-tls", feature = "local-http-rustls"))]
use std::{
    pin::Pin,
    task::{self, Poll},
};

#[cfg(any(feature = "local-http-native-tls", feature = "local-http-rustls"))]
use pin_project::pin_project;
#[cfg(any(feature = "local-http-native-tls", feature = "local-http-rustls"))]
use tokio::io::ReadBuf;

#[cfg(any(feature = "local-http-native-tls", feature = "local-http-rustls"))]
use super::stream::OutboundProxyStream;

/// Error helper for builds that disable both TLS backends.
#[inline]
pub fn tls_unsupported<T>() -> io::Result<T> {
    Err(io::Error::new(
        io::ErrorKind::Unsupported,
        "HTTPS outbound proxy requires either `local-http-native-tls` or `local-http-rustls` feature",
    ))
}

/// TLS-wrapped outbound proxy stream.
///
/// The inner I/O object is a boxed [`OutboundProxyStream`]. Boxing is purely
/// structural here: TLS libraries bake the inner type into their own
/// generics, so without indirection every additional TLS hop in a chain
/// would explode the type. There is **no** dynamic dispatch involved
/// (the `Box<OutboundProxyStream>` is a sized concrete enum value, not
/// `Box<dyn Trait>`).
#[cfg(any(feature = "local-http-native-tls", feature = "local-http-rustls"))]
#[pin_project(project = OutboundTlsStreamProj)]
pub enum OutboundTlsStream {
    #[cfg(all(feature = "local-http-native-tls", not(feature = "local-http-rustls")))]
    NativeTls(#[pin] tokio_native_tls::TlsStream<Box<OutboundProxyStream>>, bool),

View on GitHub (pinned to 8eb0f0a65b)

Solutions

  1. Rebuild with a TLS backend: --features local-http-rustls (or local-http-native-tls)
  2. Prefer the rustls backend for a pure-Rust build: cargo build --features local-http,local-http-rustls
  3. Downgrade the hop to plain HTTP or SOCKS5 if TLS is not actually required

Example fix

// before
cargo build --features local-http
// after
cargo build --features local-http,local-http-rustls
Defensive patterns

Strategy: validation

Validate before calling

#[cfg(not(any(feature = "local-http-native-tls", feature = "local-http-rustls")))]
if matches!(hop.kind, OutboundProxyKind::Https{..}) {
    return Err(anyhow!("https hop needs a TLS backend feature enabled"));
}

Type guard

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

Try / catch

if let Err(e) = tls_connect(stream, domain).await {
    if e.kind() == io::ErrorKind::Unsupported {
        eprintln!("enable local-http-native-tls or local-http-rustls");
    }
    return Err(e.into());
}

Prevention

When it happens

Trigger: Config contains an `https` outbound proxy hop while the build enables `local-http` but neither TLS backend feature; negotiate_hop calls tls_connect which hits tls_unsupported.

Common situations: Enabling only `local-http` (plain HTTP) and expecting HTTPS to work; feature matrix confusion between native-tls and rustls backends; minimal CI builds missing the TLS feature.

Understand the failure class

Related errors


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