mirror of
https://github.com/shadowsocks/shadowsocks-rust.git
synced 2026-11-06 16:14:11 +00:00
feat(service): outbound UDP relay through SOCKS5-only chains
Phase 2 of the outbound proxy refactor. The new
OutboundProxyDatagram type implements DatagramSocket / DatagramSend /
DatagramReceive on top of a freshly-bound local UDP socket plus one
SOCKS5 UDP ASSOCIATE per chain hop. Each datagram on the wire carries
N nested UdpAssociateHeader frames (one per hop), the inner-most one
addressing the ss-server's UDP external endpoint.
Per-hop control connections are built by reusing the existing TCP
chain builder (connect_chain_for_udp_associate) so all of the
shadowsocks DNS resolver / ConnectOpts behaviour still applies.
Integration:
* OutboundProxyClient::associate_udp + supports_udp().
* OutboundProxyClient::supports_udp() returns true iff every hop is
SOCKS5; HTTP/HTTPS hops cannot transport UDP and downgrade the
whole chain to 'unsupported'.
* local::net::udp::association::create_proxied_socket dispatches
based on supports_udp(): when true the per-association socket is
ProxySocket<OutboundProxyDatagram> wrapping the chained datagram;
otherwise we keep the previous direct ProxySocket<ShadowUdpSocket>.
* When the chain is configured but unable to relay UDP, sslocal /
ssserver emit a single startup WARN ('UDP traffic will bypass the
chain') instead of failing — runtime traffic transparently falls
back to a direct connection to the ss-server.
OutboundProxyStream gains try_into_tcp() so SOCKS5-only chains can
recover the bare TcpStream as keep-alive control connections —
Sync-friendly storage required because the HTTP CONNECT variant
internally holds a hyper::upgrade::Upgraded which is !Sync.
This commit is contained in:
@@ -17,22 +17,109 @@ use rand::{RngExt, rngs::SmallRng};
|
||||
use tokio::{sync::mpsc, task::JoinHandle, time};
|
||||
|
||||
use shadowsocks::{
|
||||
config::ServerConfig,
|
||||
lookup_then,
|
||||
net::{AddrFamily, UdpSocket as ShadowUdpSocket},
|
||||
net::{AddrFamily, ConnectOpts, TcpStream as ShadowTcpStream, UdpSocket as ShadowUdpSocket},
|
||||
relay::{
|
||||
Address,
|
||||
udprelay::{MAXIMUM_UDP_PAYLOAD_SIZE, ProxySocket, options::UdpSocketControlData},
|
||||
udprelay::{
|
||||
MAXIMUM_UDP_PAYLOAD_SIZE, ProxySocket,
|
||||
options::UdpSocketControlData,
|
||||
proxy_socket::UdpSocketType,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
local::{context::ServiceContext, loadbalancing::PingBalancer},
|
||||
net::{
|
||||
MonProxySocket, UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, UDP_ASSOCIATION_SEND_CHANNEL_SIZE,
|
||||
MonProxySocket, OutboundProxyDatagram, TcpDialer,
|
||||
UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, UDP_ASSOCIATION_SEND_CHANNEL_SIZE,
|
||||
packet_window::PacketWindowFilter,
|
||||
},
|
||||
};
|
||||
|
||||
/// Build the proxied socket appropriate for `svr_cfg`, transparently
|
||||
/// going through the configured outbound chain when one is present and
|
||||
/// fully SOCKS5 (otherwise the chain is bypassed for UDP).
|
||||
async fn create_proxied_socket(
|
||||
context: &ServiceContext,
|
||||
svr_cfg: &ServerConfig,
|
||||
connect_opts: &ConnectOpts,
|
||||
) -> io::Result<ProxiedSocket> {
|
||||
let use_chain = context
|
||||
.outbound_client()
|
||||
.map(|c| c.supports_udp())
|
||||
.unwrap_or(false);
|
||||
|
||||
if use_chain {
|
||||
let client = context
|
||||
.outbound_client()
|
||||
.expect("outbound_client checked above")
|
||||
.clone();
|
||||
let dialer = LocalTcpDialer {
|
||||
context: Arc::new(context.clone()),
|
||||
opts: connect_opts.clone(),
|
||||
};
|
||||
let target: Address = svr_cfg.udp_external_addr().into();
|
||||
let datagram = client
|
||||
.associate_udp(&context.context(), &dialer, connect_opts, target)
|
||||
.await?;
|
||||
let proxy_socket =
|
||||
ProxySocket::from_socket(UdpSocketType::Client, context.context(), svr_cfg, datagram);
|
||||
let mon = MonProxySocket::from_socket(proxy_socket, context.flow_stat());
|
||||
Ok(ProxiedSocket::Chained(mon))
|
||||
} else {
|
||||
let socket = ProxySocket::connect_with_opts(context.context(), svr_cfg, connect_opts).await?;
|
||||
let mon = MonProxySocket::from_socket(socket, context.flow_stat());
|
||||
Ok(ProxiedSocket::Direct(mon))
|
||||
}
|
||||
}
|
||||
|
||||
/// `TcpDialer` adapter that uses the local service context's connect options.
|
||||
struct LocalTcpDialer {
|
||||
context: Arc<ServiceContext>,
|
||||
opts: ConnectOpts,
|
||||
}
|
||||
|
||||
impl TcpDialer for LocalTcpDialer {
|
||||
async fn dial(&self, addr: &Address) -> io::Result<ShadowTcpStream> {
|
||||
ShadowTcpStream::connect_remote_with_opts(self.context.context_ref(), addr, &self.opts).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Proxied UDP socket, either direct (single hop to ss-server) or routed
|
||||
/// through a SOCKS5-only outbound proxy chain.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum ProxiedSocket {
|
||||
Direct(MonProxySocket<ShadowUdpSocket>),
|
||||
Chained(MonProxySocket<OutboundProxyDatagram>),
|
||||
}
|
||||
|
||||
impl ProxiedSocket {
|
||||
async fn send_with_ctrl(
|
||||
&self,
|
||||
addr: &Address,
|
||||
control: &UdpSocketControlData,
|
||||
data: &[u8],
|
||||
) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Direct(s) => s.send_with_ctrl(addr, control, data).await,
|
||||
Self::Chained(s) => s.send_with_ctrl(addr, control, data).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_with_ctrl(
|
||||
&self,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<(usize, Address, Option<UdpSocketControlData>)> {
|
||||
match self {
|
||||
Self::Direct(s) => s.recv_with_ctrl(buf).await,
|
||||
Self::Chained(s) => s.recv_with_ctrl(buf).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writer for sending packets back to client
|
||||
#[trait_variant::make(Send)]
|
||||
pub trait UdpInboundWrite {
|
||||
@@ -213,7 +300,7 @@ where
|
||||
peer_addr: SocketAddr,
|
||||
bypassed_ipv4_socket: Option<ShadowUdpSocket>,
|
||||
bypassed_ipv6_socket: Option<ShadowUdpSocket>,
|
||||
proxied_socket: Option<MonProxySocket<ShadowUdpSocket>>,
|
||||
proxied_socket: Option<ProxiedSocket>,
|
||||
keepalive_tx: mpsc::Sender<SocketAddr>,
|
||||
keepalive_flag: bool,
|
||||
balancer: PingBalancer,
|
||||
@@ -410,7 +497,7 @@ where
|
||||
|
||||
#[inline]
|
||||
async fn receive_from_proxied_opt(
|
||||
socket: &Option<MonProxySocket<ShadowUdpSocket>>,
|
||||
socket: &Option<ProxiedSocket>,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> io::Result<(usize, Address, Option<UdpSocketControlData>)> {
|
||||
match *socket {
|
||||
@@ -565,15 +652,17 @@ where
|
||||
Some(ref mut socket) => socket,
|
||||
None => {
|
||||
// Create a new connection to proxy server
|
||||
|
||||
let server = self.balancer.best_udp_server();
|
||||
let svr_cfg = server.server_config();
|
||||
|
||||
let socket =
|
||||
ProxySocket::connect_with_opts(self.context.context(), svr_cfg, server.connect_opts_ref()).await?;
|
||||
let socket = MonProxySocket::from_socket(socket, self.context.flow_stat());
|
||||
let proxied = create_proxied_socket(
|
||||
self.context.as_ref(),
|
||||
svr_cfg,
|
||||
server.connect_opts_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.proxied_socket.insert(socket)
|
||||
self.proxied_socket.insert(proxied)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ pub use self::{
|
||||
mon_socket::MonProxySocket,
|
||||
mon_stream::MonProxyStream,
|
||||
outbound::{
|
||||
HttpProxyAuth, OutboundProxyClient, OutboundProxyHop, OutboundProxyKind, OutboundProxyStream, Socks5Auth,
|
||||
Socks5Negotiator, TcpDialer,
|
||||
HttpProxyAuth, OutboundProxyClient, OutboundProxyDatagram, OutboundProxyHop, OutboundProxyKind,
|
||||
OutboundProxyStream, Socks5Auth, Socks5Negotiator, TcpDialer,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -70,6 +70,39 @@ where
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Build a TCP control connection that *terminates at* `hops[hop_index]`
|
||||
/// (i.e. previous hops are traversed via SOCKS5 TcpConnect, but the last
|
||||
/// hop is left in its post-handshake "ready for an arbitrary command"
|
||||
/// state — typically `UDP ASSOCIATE`).
|
||||
///
|
||||
/// All hops in `hops[..=hop_index]` must be SOCKS5; this helper is used
|
||||
/// exclusively by the UDP outbound path which only supports
|
||||
/// SOCKS5-only chains.
|
||||
pub(crate) async fn connect_chain_for_udp_associate<D>(
|
||||
hops: &[OutboundProxyHop],
|
||||
hop_index: usize,
|
||||
dialer: &D,
|
||||
) -> io::Result<OutboundProxyStream>
|
||||
where
|
||||
D: TcpDialer + Sync,
|
||||
{
|
||||
debug_assert!(hop_index < hops.len());
|
||||
let prefix = &hops[..hop_index];
|
||||
let target_hop = &hops[hop_index];
|
||||
|
||||
if hop_index == 0 {
|
||||
// Direct dial.
|
||||
let tcp = dialer.dial(&target_hop.addr).await?;
|
||||
return OutboundProxyStream::from_tcp(tcp);
|
||||
}
|
||||
|
||||
// Reuse `connect_chain`: target = hops[hop_index].addr; the chain
|
||||
// builder will SOCKS5-TcpConnect through prefix and leave the byte
|
||||
// stream pointed at `hops[hop_index]` ready for the caller to issue
|
||||
// its own SOCKS5 handshake / UdpAssociate command on top.
|
||||
connect_chain(prefix, dialer, &target_hop.addr).await
|
||||
}
|
||||
|
||||
async fn negotiate_hop(
|
||||
mut stream: OutboundProxyStream,
|
||||
hop: &OutboundProxyHop,
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
|
||||
use std::{io, sync::Arc};
|
||||
|
||||
use shadowsocks::relay::socks5::Address;
|
||||
use shadowsocks::{
|
||||
context::SharedContext,
|
||||
net::ConnectOpts,
|
||||
relay::socks5::Address,
|
||||
};
|
||||
|
||||
use crate::config::{OutboundProxy, OutboundProxyProtocol};
|
||||
|
||||
@@ -34,10 +38,12 @@ pub mod http_connect;
|
||||
pub mod socks5;
|
||||
pub mod stream;
|
||||
pub mod tls;
|
||||
pub mod udp;
|
||||
|
||||
pub use auth::{HttpProxyAuth, Socks5Auth};
|
||||
pub use socks5::Socks5Negotiator;
|
||||
pub use stream::OutboundProxyStream;
|
||||
pub use udp::OutboundProxyDatagram;
|
||||
|
||||
#[cfg(feature = "local-http")]
|
||||
pub use http_connect::{HttpConnectClient, HttpConnectTunnel};
|
||||
@@ -141,6 +147,25 @@ impl OutboundProxyClient {
|
||||
pub fn supports_udp(&self) -> bool {
|
||||
!self.hops.is_empty() && self.hops.iter().all(OutboundProxyHop::supports_udp)
|
||||
}
|
||||
|
||||
/// Establish a multi-hop UDP relay through the chain.
|
||||
///
|
||||
/// Requires every hop to be SOCKS5 (see [`Self::supports_udp`]).
|
||||
/// `target` is the inner-most destination address baked into every
|
||||
/// outgoing datagram's SOCKS5 UDP header (typically the ss-server's
|
||||
/// UDP external address).
|
||||
pub async fn associate_udp<D>(
|
||||
&self,
|
||||
context: &SharedContext,
|
||||
dialer: &D,
|
||||
connect_opts: &ConnectOpts,
|
||||
target: Address,
|
||||
) -> io::Result<OutboundProxyDatagram>
|
||||
where
|
||||
D: TcpDialer + Sync,
|
||||
{
|
||||
OutboundProxyDatagram::associate(self, context, dialer, connect_opts, target).await
|
||||
}
|
||||
}
|
||||
|
||||
fn hop_from_config(proxy: &OutboundProxy) -> OutboundProxyHop {
|
||||
|
||||
@@ -79,6 +79,25 @@ impl OutboundProxyStream {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
/// Try to recover the underlying [`TcpStream`].
|
||||
///
|
||||
/// Succeeds only if the stream is still in the unwrapped TCP state
|
||||
/// (i.e. no TLS or HTTP CONNECT layer has been applied). The UDP
|
||||
/// outbound path uses this to obtain a keep-alive connection that is
|
||||
/// `Sync`-friendly (`OutboundProxyStream` itself is intentionally not
|
||||
/// `Sync` because the HTTP CONNECT variant wraps a
|
||||
/// `hyper::upgrade::Upgraded` which is not).
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn try_into_tcp(self) -> Result<TcpStream, Self> {
|
||||
match self.inner {
|
||||
OutboundProxyStreamInner::Bypassed(s) => Ok(s),
|
||||
other => Err(Self {
|
||||
local_addr: self.local_addr,
|
||||
inner: other,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap as a TLS-protected stream (used by the chain builder when the
|
||||
/// next hop is HTTPS). `local_addr` is the address recorded for the
|
||||
/// very first TCP hop and is preserved unchanged.
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
//! Outbound UDP relay through a SOCKS5-only proxy chain.
|
||||
//!
|
||||
//! Establishes a SOCKS5 `UDP ASSOCIATE` against every hop in the
|
||||
//! configured chain, then transports each datagram by nesting one
|
||||
//! [`UdpAssociateHeader`] per hop. The resulting [`OutboundProxyDatagram`]
|
||||
//! implements [`DatagramSocket`] / [`DatagramSend`] / [`DatagramReceive`]
|
||||
//! so it can be plugged directly into [`ProxySocket::from_socket`] in
|
||||
//! place of a raw UDP socket.
|
||||
//!
|
||||
//! Supported only when **every** hop is SOCKS5 (see
|
||||
//! [`OutboundProxyClient::supports_udp`]); the dispatcher in
|
||||
//! `local::net::udp::association` is responsible for picking this path
|
||||
//! vs. the direct one.
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor},
|
||||
net::SocketAddr,
|
||||
sync::Mutex,
|
||||
task::{Context, Poll, ready},
|
||||
};
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use log::{debug, trace};
|
||||
use shadowsocks::{
|
||||
context::SharedContext,
|
||||
lookup_then,
|
||||
net::{AddrFamily, ConnectOpts, TcpStream as ShadowTcpStream, UdpSocket as ShadowUdpSocket},
|
||||
relay::{
|
||||
socks5::{Address, UdpAssociateHeader},
|
||||
udprelay::{DatagramReceive, DatagramSend, DatagramSocket},
|
||||
},
|
||||
};
|
||||
use tokio::io::ReadBuf;
|
||||
|
||||
use super::{
|
||||
OutboundProxyClient, OutboundProxyKind, TcpDialer, chain::connect_chain_for_udp_associate,
|
||||
socks5::Socks5Negotiator,
|
||||
};
|
||||
|
||||
/// One SOCKS5 UDP relay hop. The `assoc_tcp` keep-alive connection must
|
||||
/// outlive every datagram exchanged through this hop (RFC 1928).
|
||||
struct Socks5UdpRelay {
|
||||
/// Keep-alive TCP control connection. We hold the bare [`ShadowTcpStream`]
|
||||
/// rather than an [`super::OutboundProxyStream`] so the relay (and
|
||||
/// therefore [`OutboundProxyDatagram`]) is `Sync` — the chain builder
|
||||
/// never produces a non-TCP variant for SOCKS5-only chains, so this
|
||||
/// downcast always succeeds.
|
||||
#[allow(dead_code)]
|
||||
assoc_tcp: ShadowTcpStream,
|
||||
relay_addr: Address,
|
||||
relay_socket_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl Socks5UdpRelay {
|
||||
fn relay_addr(&self) -> &Address {
|
||||
&self.relay_addr
|
||||
}
|
||||
}
|
||||
|
||||
/// Multi-hop SOCKS5 UDP relay implementing the
|
||||
/// [`DatagramSocket`] / [`DatagramSend`] / [`DatagramReceive`] triple.
|
||||
///
|
||||
/// The chain is built once at association time. Each `send` then nests
|
||||
/// `hops` SOCKS5 UDP headers around the upper-layer payload (the inner-
|
||||
/// most header carries the **final target** address — for sslocal /
|
||||
/// ssserver this is always the configured `target` baked in at
|
||||
/// construction time, namely the ss-server's UDP external address).
|
||||
pub struct OutboundProxyDatagram {
|
||||
socket: ShadowUdpSocket,
|
||||
relays: Vec<Socks5UdpRelay>,
|
||||
/// Inner-most target address — recorded once at construction time
|
||||
/// and used as the SOCKS5 UDP innermost destination on every send.
|
||||
/// For sslocal / ssserver outbound chains this is the ss-server's
|
||||
/// UDP external address.
|
||||
target: Address,
|
||||
/// Address the local UDP socket is `connect()`ed to (the first hop's
|
||||
/// relay address). Reported to upper layers as the "peer" / source
|
||||
/// from which `recv_from` returns datagrams.
|
||||
first_hop_addr: SocketAddr,
|
||||
/// Reusable receive scratch buffer (header stripping requires reading
|
||||
/// the full datagram before we know the payload offset).
|
||||
recv_scratch: Mutex<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl OutboundProxyDatagram {
|
||||
/// Establish UDP relays through every hop of `client`.
|
||||
///
|
||||
/// * `target` is the inner-most destination (typically the ss-server's
|
||||
/// UDP external address).
|
||||
/// * `dialer` is used to dial all TCP control connections.
|
||||
/// * `connect_opts` configures the local UDP socket.
|
||||
pub async fn associate<D>(
|
||||
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
|
||||
// previous hop.
|
||||
let mut announce: Address = Address::from(local_udp_addr);
|
||||
|
||||
for (idx, hop) in hops.iter().enumerate() {
|
||||
// Build a TCP control connection that *terminates* at `hop`
|
||||
// (i.e. previous hops are traversed via SOCKS5 TcpConnect).
|
||||
let mut tcp = connect_chain_for_udp_associate(hops, idx, dialer).await?;
|
||||
|
||||
let auth = match &hop.kind {
|
||||
OutboundProxyKind::Socks5 { auth } => auth,
|
||||
_ => unreachable!("guarded above"),
|
||||
};
|
||||
|
||||
let resp = Socks5Negotiator::establish_udp_associate(&mut tcp, announce.clone(), auth)
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
// For a SOCKS5-only chain the byte stream is always still raw
|
||||
// TCP after the handshake (no TLS / HTTP layers were
|
||||
// applied). Recover the underlying TcpStream so the relay
|
||||
// struct stays `Sync`.
|
||||
let tcp = tcp.try_into_tcp().map_err(|_| {
|
||||
io::Error::other(
|
||||
"internal error: outbound UDP relay produced a non-TCP keep-alive stream",
|
||||
)
|
||||
})?;
|
||||
|
||||
let relay_addr = resp.address;
|
||||
let relay_socket_addr = match relay_addr {
|
||||
Address::SocketAddress(sa) => Some(sa),
|
||||
Address::DomainNameAddress(ref name, port) => {
|
||||
// Resolve domain-name relay addresses via the
|
||||
// shadowsocks shared resolver — matches what a direct
|
||||
// ProxySocket::connect_with_opts does.
|
||||
let (sa, _) = lookup_then!(context, name, port, |sa| {
|
||||
Ok::<SocketAddr, io::Error>(sa)
|
||||
})?;
|
||||
Some(sa)
|
||||
}
|
||||
};
|
||||
|
||||
trace!(
|
||||
"outbound udp hop {}: relay_addr={} (resolved={:?}), announce={}",
|
||||
idx, relay_addr, relay_socket_addr, announce
|
||||
);
|
||||
|
||||
// Next hop's announce = this hop's relay address.
|
||||
announce = match relay_socket_addr {
|
||||
Some(sa) => Address::from(sa),
|
||||
None => relay_addr.clone(),
|
||||
};
|
||||
|
||||
relays.push(Socks5UdpRelay {
|
||||
assoc_tcp: tcp,
|
||||
relay_addr,
|
||||
relay_socket_addr,
|
||||
});
|
||||
}
|
||||
|
||||
let first_hop_addr = relays[0]
|
||||
.relay_socket_addr
|
||||
.ok_or_else(|| io::Error::other("first SOCKS5 UDP relay returned an unresolved domain"))?;
|
||||
|
||||
// Connect the local UDP socket to the first hop so subsequent
|
||||
// sends can use `send` instead of `send_to`.
|
||||
socket.connect(first_hop_addr).await?;
|
||||
|
||||
debug!(
|
||||
"outbound udp chain established: {} hop(s), first_hop={}, target={}",
|
||||
relays.len(),
|
||||
first_hop_addr,
|
||||
target,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
socket,
|
||||
relays,
|
||||
target,
|
||||
first_hop_addr,
|
||||
recv_scratch: Mutex::new(Vec::new()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Number of SOCKS5 hops the chain consists of.
|
||||
pub fn hop_count(&self) -> usize {
|
||||
self.relays.len()
|
||||
}
|
||||
|
||||
/// Wrap `payload` in N nested SOCKS5 UDP headers, where N is the
|
||||
/// number of hops. The inner-most header carries `self.target`.
|
||||
fn encode(&self, payload: &[u8]) -> BytesMut {
|
||||
let n = self.relays.len();
|
||||
|
||||
// Header[i] addr = relays[i+1].relay_addr() for i < n-1, or
|
||||
// self.target for i == n-1.
|
||||
let mut total = payload.len();
|
||||
for i in 0..n {
|
||||
let addr = if i + 1 < n {
|
||||
self.relays[i + 1].relay_addr()
|
||||
} else {
|
||||
&self.target
|
||||
};
|
||||
total += UdpAssociateHeader::new(0, addr.clone()).serialized_len();
|
||||
}
|
||||
|
||||
let mut buf = BytesMut::with_capacity(total);
|
||||
for i in 0..n {
|
||||
let addr = if i + 1 < n {
|
||||
self.relays[i + 1].relay_addr().clone()
|
||||
} else {
|
||||
self.target.clone()
|
||||
};
|
||||
UdpAssociateHeader::new(0, addr).write_to_buf(&mut buf);
|
||||
}
|
||||
buf.put_slice(payload);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Strip N nested SOCKS5 UDP headers from a freshly received datagram.
|
||||
/// Returns the inner-most reported source address and the payload
|
||||
/// offset.
|
||||
fn decode(&self, recv_buf: &[u8]) -> io::Result<(Address, usize)> {
|
||||
let n = self.relays.len();
|
||||
let mut cur = Cursor::new(recv_buf);
|
||||
let mut last_addr: Option<Address> = None;
|
||||
|
||||
for _ in 0..n {
|
||||
// `UdpAssociateHeader::read_from` is async over an
|
||||
// `AsyncRead`. Cursor's `AsyncRead` impl yields synchronously,
|
||||
// so polling the future once always completes.
|
||||
let fut = std::pin::pin!(UdpAssociateHeader::read_from(&mut cur));
|
||||
match futures::FutureExt::now_or_never(fut) {
|
||||
Some(Ok(h)) => last_addr = Some(h.address),
|
||||
Some(Err(err)) => {
|
||||
return Err(io::Error::other(format!(
|
||||
"outbound udp: failed to parse SOCKS5 UDP header: {err}"
|
||||
)));
|
||||
}
|
||||
None => unreachable!("Cursor read_from should be synchronous"),
|
||||
}
|
||||
}
|
||||
|
||||
let pos = cur.position() as usize;
|
||||
Ok((last_addr.expect("relays is non-empty"), pos))
|
||||
}
|
||||
}
|
||||
|
||||
impl DatagramSocket for OutboundProxyDatagram {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
self.socket.local_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl DatagramSend for OutboundProxyDatagram {
|
||||
fn poll_send(&self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
|
||||
let wire = self.encode(buf);
|
||||
let sent = ready!(self.socket.poll_send(cx, &wire))?;
|
||||
if sent == wire.len() {
|
||||
// Report the upper-layer payload size (matches what
|
||||
// ProxySocket expects from a direct UDP send).
|
||||
Poll::Ready(Ok(buf.len()))
|
||||
} else {
|
||||
Poll::Ready(Err(io::Error::from(io::ErrorKind::WriteZero)))
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_send_to(&self, cx: &mut Context<'_>, buf: &[u8], _target: SocketAddr) -> Poll<io::Result<usize>> {
|
||||
// The first-hop relay is what we actually send to on the wire;
|
||||
// the upper layer's `target` (typically the ss-server) is already
|
||||
// baked into the inner-most SOCKS5 UDP header via `self.target`.
|
||||
let _ = _target;
|
||||
self.poll_send(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_send_ready(&self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.socket.poll_send_ready(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl DatagramReceive for OutboundProxyDatagram {
|
||||
fn poll_recv(&self, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
|
||||
// Receive into our scratch, strip headers, then copy payload into
|
||||
// the caller's buffer.
|
||||
let mut scratch = self.recv_scratch.lock().expect("recv scratch poisoned");
|
||||
let needed = buf.remaining() + 1024;
|
||||
if scratch.len() < needed {
|
||||
scratch.resize(needed, 0);
|
||||
}
|
||||
let mut tmp = ReadBuf::new(&mut scratch);
|
||||
ready!(self.socket.poll_recv(cx, &mut tmp))?;
|
||||
let n = tmp.filled().len();
|
||||
let (_inner_src, pos) = self.decode(&scratch[..n])?;
|
||||
let payload_len = n - pos;
|
||||
if payload_len > buf.remaining() {
|
||||
return Poll::Ready(Err(io::Error::from(io::ErrorKind::InvalidData)));
|
||||
}
|
||||
buf.put_slice(&scratch[pos..n]);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_recv_from(&self, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<SocketAddr>> {
|
||||
// The upper layer wants the source address. We report the first-
|
||||
// hop relay we actually received from (the only socket-level
|
||||
// address available); the inner-most SOCKS5 source is discarded
|
||||
// because the upper layer will decrypt and parse the ss-server's
|
||||
// own packet header anyway.
|
||||
ready!(self.poll_recv(cx, buf))?;
|
||||
Poll::Ready(Ok(self.first_hop_addr))
|
||||
}
|
||||
|
||||
fn poll_recv_ready(&self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.socket.poll_recv_ready(cx)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user