Allow Shadowsocks upstreams in middle-proxy mode

Co-authored-by: Maxim Myalin <maxim@myalin.ru>
This commit is contained in:
Cursor Agent 2026-03-22 12:37:39 +00:00
parent 2e8bfa1101
commit 140bf0e180
No known key found for this signature in database
7 changed files with 154 additions and 47 deletions

View File

@ -122,11 +122,11 @@ enabled = true
``` ```
#### Shadowsocks as Upstream #### Shadowsocks as Upstream
Requires `use_middle_proxy = false`. Works with both `use_middle_proxy = false` and `use_middle_proxy = true`.
```toml ```toml
[general] [general]
use_middle_proxy = false use_middle_proxy = true
[[upstreams]] [[upstreams]]
type = "shadowsocks" type = "shadowsocks"

View File

@ -117,7 +117,7 @@ Defaults below are code defaults (used when a key is omitted), not necessarily v
8. In ME mode, the selected upstream is also used for ME TCP dial path. 8. In ME mode, the selected upstream is also used for ME TCP dial path.
9. In ME mode for `direct` upstream with bind/interface, STUN reflection logic is bind-aware for KDF source material. 9. In ME mode for `direct` upstream with bind/interface, STUN reflection logic is bind-aware for KDF source material.
10. In ME mode for SOCKS upstream, SOCKS `BND.ADDR/BND.PORT` is used for KDF when it is valid/public for the same family. 10. In ME mode for SOCKS upstream, SOCKS `BND.ADDR/BND.PORT` is used for KDF when it is valid/public for the same family.
11. `shadowsocks` upstreams require `general.use_middle_proxy = false`. Config load fails fast if ME mode is enabled. 11. `shadowsocks` upstreams work in both Direct and ME modes. In ME mode, the connected local Shadowsocks address is reused for bind-aware STUN reflection when available.
## Upstream Configuration Examples ## Upstream Configuration Examples
@ -157,7 +157,7 @@ enabled = true
```toml ```toml
[general] [general]
use_middle_proxy = false use_middle_proxy = true
[[upstreams]] [[upstreams]]
type = "shadowsocks" type = "shadowsocks"

View File

@ -129,16 +129,6 @@ fn sanitize_ad_tag(ad_tag: &mut Option<String>) {
} }
fn validate_upstreams(config: &ProxyConfig) -> Result<()> { fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
let has_enabled_shadowsocks = config.upstreams.iter().any(|upstream| {
upstream.enabled && matches!(upstream.upstream_type, UpstreamType::Shadowsocks { .. })
});
if has_enabled_shadowsocks && config.general.use_middle_proxy {
return Err(ProxyError::Config(
"shadowsocks upstreams require general.use_middle_proxy = false".to_string(),
));
}
for upstream in &config.upstreams { for upstream in &config.upstreams {
if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type { if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type {
let parsed = ShadowsocksServerConfig::from_url(url) let parsed = ShadowsocksServerConfig::from_url(url)
@ -2275,7 +2265,7 @@ mod tests {
} }
#[test] #[test]
fn shadowsocks_requires_direct_mode() { fn shadowsocks_is_allowed_with_middle_proxy() {
let toml = format!( let toml = format!(
r#" r#"
[general] [general]
@ -2294,11 +2284,11 @@ mod tests {
url = TEST_SHADOWSOCKS_URL, url = TEST_SHADOWSOCKS_URL,
); );
let dir = std::env::temp_dir(); let dir = std::env::temp_dir();
let path = dir.join("telemt_shadowsocks_me_reject_test.toml"); let path = dir.join("telemt_shadowsocks_me_allow_test.toml");
std::fs::write(&path, toml).unwrap(); std::fs::write(&path, toml).unwrap();
let err = ProxyConfig::load(&path).unwrap_err().to_string(); let loaded = ProxyConfig::load(&path);
assert!(err.contains("shadowsocks upstreams require general.use_middle_proxy = false")); assert!(loaded.is_ok());
let _ = std::fs::remove_file(path); let _ = std::fs::remove_file(path);
} }

View File

@ -4,8 +4,9 @@ use bytes::Bytes;
use crate::crypto::{AesCbc, crc32, crc32c}; use crate::crypto::{AesCbc, crc32, crc32c};
use crate::error::{ProxyError, Result}; use crate::error::{ProxyError, Result};
use crate::protocol::constants::*; use crate::protocol::constants::*;
use crate::transport::UpstreamStream;
/// Commands sent to dedicated writer tasks to avoid mutex contention on TCP writes. /// Commands sent to dedicated writer tasks to avoid mutex contention on upstream writes.
pub(crate) enum WriterCommand { pub(crate) enum WriterCommand {
Data(Bytes), Data(Bytes),
DataAndFlush(Bytes), DataAndFlush(Bytes),
@ -213,7 +214,7 @@ pub(crate) fn cbc_decrypt_inplace(
} }
pub(crate) struct RpcWriter { pub(crate) struct RpcWriter {
pub(crate) writer: tokio::io::WriteHalf<tokio::net::TcpStream>, pub(crate) writer: tokio::io::WriteHalf<UpstreamStream>,
pub(crate) key: [u8; 32], pub(crate) key: [u8; 32],
pub(crate) iv: [u8; 16], pub(crate) iv: [u8; 16],
pub(crate) seq_no: i32, pub(crate) seq_no: i32,

View File

@ -1,13 +1,15 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use socket2::{SockRef, TcpKeepalive}; use socket2::{SockRef, TcpKeepalive};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use libc; use libc;
#[cfg(unix)]
use std::os::fd::BorrowedFd;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::os::fd::{AsRawFd, RawFd}; use std::os::fd::RawFd;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::os::raw::c_int; use std::os::raw::c_int;
@ -26,7 +28,7 @@ use crate::protocol::constants::{
ME_CONNECT_TIMEOUT_SECS, ME_HANDSHAKE_TIMEOUT_SECS, RPC_CRYPTO_AES_U32, ME_CONNECT_TIMEOUT_SECS, ME_HANDSHAKE_TIMEOUT_SECS, RPC_CRYPTO_AES_U32,
RPC_HANDSHAKE_ERROR_U32, rpc_crypto_flags, RPC_HANDSHAKE_ERROR_U32, rpc_crypto_flags,
}; };
use crate::transport::{UpstreamEgressInfo, UpstreamRouteKind}; use crate::transport::{UpstreamEgressInfo, UpstreamRouteKind, UpstreamStream};
use super::codec::{ use super::codec::{
RpcChecksumMode, build_handshake_payload, build_nonce_payload, build_rpc_frame, RpcChecksumMode, build_handshake_payload, build_nonce_payload, build_rpc_frame,
@ -57,8 +59,8 @@ impl KdfClientPortSource {
/// Result of a successful ME handshake with timings. /// Result of a successful ME handshake with timings.
pub(crate) struct HandshakeOutput { pub(crate) struct HandshakeOutput {
pub rd: ReadHalf<TcpStream>, pub rd: ReadHalf<UpstreamStream>,
pub wr: WriteHalf<TcpStream>, pub wr: WriteHalf<UpstreamStream>,
pub source_ip: IpAddr, pub source_ip: IpAddr,
pub read_key: [u8; 32], pub read_key: [u8; 32],
pub read_iv: [u8; 16], pub read_iv: [u8; 16],
@ -89,15 +91,19 @@ impl MePool {
i16::try_from(self.resolve_dc_for_endpoint(addr).await).ok() i16::try_from(self.resolve_dc_for_endpoint(addr).await).ok()
} }
fn direct_bind_ip_for_stun( fn non_socks_bind_ip_for_stun(
family: IpFamily, family: IpFamily,
upstream_egress: Option<UpstreamEgressInfo>, upstream_egress: Option<UpstreamEgressInfo>,
) -> Option<IpAddr> { ) -> Option<IpAddr> {
let info = upstream_egress?; let info = upstream_egress?;
if info.route_kind != UpstreamRouteKind::Direct { let bind_ip = match info.route_kind {
return None; UpstreamRouteKind::Direct => info
} .direct_bind_ip
match (family, info.direct_bind_ip) { .or_else(|| info.local_addr.map(|addr| addr.ip())),
UpstreamRouteKind::Shadowsocks => info.local_addr.map(|addr| addr.ip()),
UpstreamRouteKind::Socks4 | UpstreamRouteKind::Socks5 => None,
};
match (family, bind_ip) {
(IpFamily::V4, Some(IpAddr::V4(ip))) => Some(IpAddr::V4(ip)), (IpFamily::V4, Some(IpAddr::V4(ip))) => Some(IpAddr::V4(ip)),
(IpFamily::V6, Some(IpAddr::V6(ip))) => Some(IpAddr::V6(ip)), (IpFamily::V6, Some(IpAddr::V6(ip))) => Some(IpAddr::V6(ip)),
_ => None, _ => None,
@ -141,12 +147,12 @@ impl MePool {
} }
} }
/// TCP connect with timeout + return RTT in milliseconds. /// Connect to a middle-proxy endpoint and return RTT in milliseconds.
pub(crate) async fn connect_tcp( pub(crate) async fn connect_tcp(
&self, &self,
addr: SocketAddr, addr: SocketAddr,
dc_idx_override: Option<i16>, dc_idx_override: Option<i16>,
) -> Result<(TcpStream, f64, Option<UpstreamEgressInfo>)> { ) -> Result<(UpstreamStream, f64, Option<UpstreamEgressInfo>)> {
let start = Instant::now(); let start = Instant::now();
let (stream, upstream_egress) = if let Some(upstream) = &self.upstream { let (stream, upstream_egress) = if let Some(upstream) = &self.upstream {
let dc_idx = if let Some(dc_idx) = dc_idx_override { let dc_idx = if let Some(dc_idx) = dc_idx_override {
@ -154,7 +160,9 @@ impl MePool {
} else { } else {
self.resolve_dc_idx_for_endpoint(addr).await self.resolve_dc_idx_for_endpoint(addr).await
}; };
let (stream, egress) = upstream.connect_with_details(addr, dc_idx, None).await?; let (stream, egress) = upstream
.connect_stream_with_details(addr, dc_idx, None)
.await?;
(stream, Some(egress)) (stream, Some(egress))
} else { } else {
let connect_fut = async { let connect_fut = async {
@ -183,7 +191,7 @@ impl MePool {
.map_err(|_| ProxyError::ConnectionTimeout { .map_err(|_| ProxyError::ConnectionTimeout {
addr: addr.to_string(), addr: addr.to_string(),
})??; })??;
(stream, None) (UpstreamStream::Tcp(stream), None)
}; };
let connect_ms = start.elapsed().as_secs_f64() * 1000.0; let connect_ms = start.elapsed().as_secs_f64() * 1000.0;
@ -192,14 +200,22 @@ impl MePool {
warn!(error = %e, "ME keepalive setup failed"); warn!(error = %e, "ME keepalive setup failed");
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Err(e) = Self::configure_user_timeout(stream.as_raw_fd()) { if let Err(e) = Self::configure_user_timeout(stream.raw_fd()) {
warn!(error = %e, "ME TCP_USER_TIMEOUT setup failed"); warn!(error = %e, "ME TCP_USER_TIMEOUT setup failed");
} }
Ok((stream, connect_ms, upstream_egress)) Ok((stream, connect_ms, upstream_egress))
} }
fn configure_keepalive(stream: &TcpStream) -> std::io::Result<()> { fn configure_keepalive(stream: &UpstreamStream) -> std::io::Result<()> {
let sock = SockRef::from(stream); #[cfg(unix)]
let borrowed = unsafe { BorrowedFd::borrow_raw(stream.raw_fd()) };
#[cfg(unix)]
let sock = SockRef::from(&borrowed);
#[cfg(not(unix))]
let sock = match stream {
UpstreamStream::Tcp(stream) => SockRef::from(stream),
UpstreamStream::Shadowsocks(_) => return Ok(()),
};
let ka = TcpKeepalive::new().with_time(Duration::from_secs(30)); let ka = TcpKeepalive::new().with_time(Duration::from_secs(30));
// Mirror socket2 v0.5.10 target gate for with_retries(), the stricter method. // Mirror socket2 v0.5.10 target gate for with_retries(), the stricter method.
@ -243,11 +259,11 @@ impl MePool {
Ok(()) Ok(())
} }
/// Perform full ME RPC handshake on an established TCP stream. /// Perform full ME RPC handshake on an established upstream stream.
/// Returns cipher keys/ivs and split halves; does not register writer. /// Returns cipher keys/ivs and split halves; does not register writer.
pub(crate) async fn handshake_only( pub(crate) async fn handshake_only(
&self, &self,
stream: TcpStream, stream: UpstreamStream,
addr: SocketAddr, addr: SocketAddr,
upstream_egress: Option<UpstreamEgressInfo>, upstream_egress: Option<UpstreamEgressInfo>,
rng: &SecureRandom, rng: &SecureRandom,
@ -300,7 +316,7 @@ impl MePool {
MeSocksKdfPolicy::Compat => { MeSocksKdfPolicy::Compat => {
self.stats.increment_me_socks_kdf_compat_fallback(); self.stats.increment_me_socks_kdf_compat_fallback();
if self.nat_probe { if self.nat_probe {
let bind_ip = Self::direct_bind_ip_for_stun(family, upstream_egress); let bind_ip = Self::non_socks_bind_ip_for_stun(family, upstream_egress);
self.maybe_reflect_public_addr(family, bind_ip).await self.maybe_reflect_public_addr(family, bind_ip).await
} else { } else {
None None
@ -308,7 +324,7 @@ impl MePool {
} }
} }
} else if self.nat_probe { } else if self.nat_probe {
let bind_ip = Self::direct_bind_ip_for_stun(family, upstream_egress); let bind_ip = Self::non_socks_bind_ip_for_stun(family, upstream_egress);
self.maybe_reflect_public_addr(family, bind_ip).await self.maybe_reflect_public_addr(family, bind_ip).await
} else { } else {
None None
@ -722,6 +738,8 @@ mod tests {
use std::io::ErrorKind; use std::io::ErrorKind;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use crate::transport::{UpstreamEgressInfo, UpstreamRouteKind, UpstreamStream};
#[tokio::test] #[tokio::test]
async fn test_configure_keepalive_loopback() { async fn test_configure_keepalive_loopback() {
let listener = match TcpListener::bind("127.0.0.1:0").await { let listener = match TcpListener::bind("127.0.0.1:0").await {
@ -740,6 +758,7 @@ mod tests {
Err(error) if error.kind() == ErrorKind::PermissionDenied => return, Err(error) if error.kind() == ErrorKind::PermissionDenied => return,
Err(error) => panic!("connect failed: {error}"), Err(error) => panic!("connect failed: {error}"),
}; };
let stream = UpstreamStream::Tcp(stream);
if let Err(error) = MePool::configure_keepalive(&stream) { if let Err(error) = MePool::configure_keepalive(&stream) {
if error.kind() == ErrorKind::PermissionDenied { if error.kind() == ErrorKind::PermissionDenied {
@ -777,4 +796,56 @@ mod tests {
.with_interval(Duration::from_secs(10)) .with_interval(Duration::from_secs(10))
.with_retries(3); .with_retries(3);
} }
#[test]
fn direct_route_prefers_explicit_bind_ip_for_stun() {
let bind_ip: IpAddr = "198.51.100.10".parse().unwrap();
let local_addr: SocketAddr = "198.51.100.20:40000".parse().unwrap();
let egress = UpstreamEgressInfo {
upstream_id: 0,
route_kind: UpstreamRouteKind::Direct,
local_addr: Some(local_addr),
direct_bind_ip: Some(bind_ip),
socks_bound_addr: None,
socks_proxy_addr: None,
};
let selected = MePool::non_socks_bind_ip_for_stun(IpFamily::V4, Some(egress));
assert_eq!(selected, Some(bind_ip));
}
#[test]
fn shadowsocks_route_uses_local_addr_for_stun() {
let local_addr: SocketAddr = "198.51.100.30:40001".parse().unwrap();
let egress = UpstreamEgressInfo {
upstream_id: 1,
route_kind: UpstreamRouteKind::Shadowsocks,
local_addr: Some(local_addr),
direct_bind_ip: None,
socks_bound_addr: None,
socks_proxy_addr: None,
};
let selected = MePool::non_socks_bind_ip_for_stun(IpFamily::V4, Some(egress));
assert_eq!(selected, Some(local_addr.ip()));
}
#[test]
fn socks_route_keeps_compat_fallback_unbound() {
let local_addr: SocketAddr = "198.51.100.40:40002".parse().unwrap();
let egress = UpstreamEgressInfo {
upstream_id: 2,
route_kind: UpstreamRouteKind::Socks5,
local_addr: Some(local_addr),
direct_bind_ip: None,
socks_bound_addr: None,
socks_proxy_addr: Some("198.51.100.50:1080".parse().unwrap()),
};
let selected = MePool::non_socks_bind_ip_for_stun(IpFamily::V4, Some(egress));
assert_eq!(selected, None);
}
} }

View File

@ -6,7 +6,6 @@ use std::time::Instant;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::net::TcpStream;
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{Mutex, mpsc};
use tokio::sync::mpsc::error::TrySendError; use tokio::sync::mpsc::error::TrySendError;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@ -16,13 +15,14 @@ use crate::crypto::AesCbc;
use crate::error::{ProxyError, Result}; use crate::error::{ProxyError, Result};
use crate::protocol::constants::*; use crate::protocol::constants::*;
use crate::stats::Stats; use crate::stats::Stats;
use crate::transport::UpstreamStream;
use super::codec::{RpcChecksumMode, WriterCommand, rpc_crc}; use super::codec::{RpcChecksumMode, WriterCommand, rpc_crc};
use super::registry::RouteResult; use super::registry::RouteResult;
use super::{ConnRegistry, MeResponse}; use super::{ConnRegistry, MeResponse};
pub(crate) async fn reader_loop( pub(crate) async fn reader_loop(
mut rd: tokio::io::ReadHalf<TcpStream>, mut rd: tokio::io::ReadHalf<UpstreamStream>,
dk: [u8; 32], dk: [u8; 32],
mut div: [u8; 16], mut div: [u8; 16],
crc_mode: RpcChecksumMode, crc_mode: RpcChecksumMode,

View File

@ -12,6 +12,8 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::Duration; use std::time::Duration;
#[cfg(unix)]
use std::os::fd::{AsRawFd, RawFd};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -173,6 +175,7 @@ pub struct StartupPingResult {
pub both_available: bool, pub both_available: bool,
} }
/// Transport stream returned by an upstream connection attempt.
pub enum UpstreamStream { pub enum UpstreamStream {
Tcp(TcpStream), Tcp(TcpStream),
Shadowsocks(Box<ShadowsocksStream>), Shadowsocks(Box<ShadowsocksStream>),
@ -188,6 +191,35 @@ impl std::fmt::Debug for UpstreamStream {
} }
impl UpstreamStream { impl UpstreamStream {
pub(crate) fn local_addr(&self) -> std::io::Result<SocketAddr> {
match self {
Self::Tcp(stream) => stream.local_addr(),
Self::Shadowsocks(stream) => stream.get_ref().local_addr(),
}
}
pub(crate) fn peer_addr(&self) -> std::io::Result<SocketAddr> {
match self {
Self::Tcp(stream) => stream.peer_addr(),
Self::Shadowsocks(stream) => stream.get_ref().peer_addr(),
}
}
pub(crate) fn set_nodelay(&self, nodelay: bool) -> std::io::Result<()> {
match self {
Self::Tcp(stream) => stream.set_nodelay(nodelay),
Self::Shadowsocks(stream) => stream.get_ref().set_nodelay(nodelay),
}
}
#[cfg(unix)]
pub(crate) fn raw_fd(&self) -> RawFd {
match self {
Self::Tcp(stream) => stream.as_raw_fd(),
Self::Shadowsocks(stream) => stream.get_ref().as_raw_fd(),
}
}
pub fn into_tcp(self) -> Result<TcpStream> { pub fn into_tcp(self) -> Result<TcpStream> {
match self { match self {
Self::Tcp(stream) => Ok(stream), Self::Tcp(stream) => Ok(stream),
@ -744,13 +776,13 @@ impl UpstreamManager {
Ok(stream) Ok(stream)
} }
/// Connect to target through a selected upstream and return egress details. /// Connect to target through a selected upstream and return transport egress details.
pub async fn connect_with_details( pub(crate) async fn connect_stream_with_details(
&self, &self,
target: SocketAddr, target: SocketAddr,
dc_idx: Option<i16>, dc_idx: Option<i16>,
scope: Option<&str>, scope: Option<&str>,
) -> Result<(TcpStream, UpstreamEgressInfo)> { ) -> Result<(UpstreamStream, UpstreamEgressInfo)> {
let idx = self let idx = self
.select_upstream(dc_idx, scope) .select_upstream(dc_idx, scope)
.await .await
@ -774,6 +806,19 @@ impl UpstreamManager {
let (stream, egress) = self let (stream, egress) = self
.connect_selected_upstream(idx, upstream, target, dc_idx, bind_rr) .connect_selected_upstream(idx, upstream, target, dc_idx, bind_rr)
.await?; .await?;
Ok((stream, egress))
}
/// Connect to target through a selected upstream and return egress details.
pub async fn connect_with_details(
&self,
target: SocketAddr,
dc_idx: Option<i16>,
scope: Option<&str>,
) -> Result<(TcpStream, UpstreamEgressInfo)> {
let (stream, egress) = self
.connect_stream_with_details(target, dc_idx, scope)
.await?;
Ok((stream.into_tcp()?, egress)) Ok((stream.into_tcp()?, egress))
} }