Skip to main content

rama_socks5/server/
mod.rs

1//! Socks5 Server Implementation for Rama.
2//!
3//! See [`Socks5Acceptor`] for more information,
4//! its [`Default`] implementation only
5//! supports the [`Command::Connect`] method using the [`DefaultConnector`],
6//! but custom connectors as well as binders and udp associators
7//! are optionally possible.
8//!
9//! MITM proxies should normally keep the connector and pass its
10//! pre-established ingress/egress [`rama_core::io::BridgeIo`] to Rama's
11//! Relay/Peek services.
12//! Use [`LazyConnector`] only when application bytes are genuinely required to
13//! choose the egress target.
14
15use crate::proto::{
16    Command, ProtocolError, ReplyKind, SocksMethod, client,
17    server::{Header, Reply, UsernamePasswordResponse},
18};
19use rama_core::{
20    Service,
21    error::BoxError,
22    extensions::{Extensions, ExtensionsRef},
23    io::Io,
24    rt::Executor,
25    telemetry::tracing,
26};
27use rama_net::{
28    address::SocketAddress,
29    extensions::StreamTransformed,
30    user::{self, authority::Authorizer},
31};
32use rama_tcp::{TcpStream, server::TcpListener};
33use std::{fmt, sync::Arc};
34
35mod peek;
36#[doc(inline)]
37pub use peek::{NoSocks5RejectError, Socks5PeekRouter, Socks5PrefixedIo};
38
39mod connect;
40pub use connect::{Connector, DefaultConnector, LazyConnector, Socks5Connector};
41
42pub mod bind;
43pub use bind::{Binder, DefaultBinder, Socks5Binder};
44
45pub mod udp;
46pub use udp::{DefaultUdpRelay, Socks5UdpAssociator, UdpRelay};
47
48/// Socks5 server implementation of [RFC 1928]
49///
50/// [RFC 1928]: https://datatracker.ietf.org/doc/html/rfc1928
51///
52/// An instance constructed with [`Socks5Acceptor::new`]
53/// is one that accepts none of the available [`Command`]s,
54/// until you embed one or more of: connector, binder and udp associator.
55///
56/// # [`Default`]
57///
58/// The [`Default`] implementation of the [`Socks5Acceptor`] only
59/// supports the [`Command::Connect`] method using the [`DefaultConnector`],
60/// but custom connectors as well as binders and udp associators
61/// are optionally possible.
62#[derive(Debug, Clone)]
63pub struct Socks5Acceptor<C = DefaultConnector, B = (), U = (), A = ()> {
64    connector: C,
65    binder: B,
66    udp_associator: U,
67
68    auth: AuthKind<A>,
69
70    // opt-in flag which allows even if server has auth configured
71    // to also support a client which doesn't support username-password auth,
72    // despite it normally working with authentication.
73    //
74    // This can be useful in case you also wish to support guest users.
75    auth_opt: bool,
76
77    exec: Executor,
78}
79
80#[derive(Debug, Clone)]
81enum AuthKind<A> {
82    NoAuth(A),
83    WithAuth(A),
84}
85
86impl Socks5Acceptor<(), (), (), ()> {
87    /// Create a new [`Socks5Acceptor`] which supports none of the valid [`Command`]s.
88    ///
89    /// Use [`Socks5Acceptor::default`] instead if you wish to create a default
90    /// [`Socks5Acceptor`] which can be used as a simple and honest byte-byte proxy.
91    #[must_use]
92    pub fn new(exec: Executor) -> Self {
93        Self {
94            connector: (),
95            binder: (),
96            udp_associator: (),
97            auth: AuthKind::NoAuth(()),
98            auth_opt: false,
99            exec,
100        }
101    }
102}
103
104impl<C, B, U> Socks5Acceptor<C, B, U> {
105    pub fn with_authorizer<A>(self, authorizer: A) -> Socks5Acceptor<C, B, U, A> {
106        Socks5Acceptor {
107            connector: self.connector,
108            binder: self.binder,
109            udp_associator: self.udp_associator,
110            auth: AuthKind::WithAuth(authorizer),
111            auth_opt: self.auth_opt,
112            exec: self.exec,
113        }
114    }
115
116    rama_utils::macros::generate_set_and_with! {
117        /// Define whether or not the authentication (if supported by this [`Socks5Acceptor`]) is optional,
118        /// by default it is no optional.
119        ///
120        /// Making authentication optional, despite supporting authentication on server side,
121        /// can be useful in case you wish to support so called Guest users.
122        pub fn auth_optional(mut self, optional: bool) -> Self {
123            self.auth_opt = optional;
124            self
125        }
126    }
127}
128
129impl<B, U, A> Socks5Acceptor<(), B, U, A> {
130    /// Attach a [`Socks5Connector`] to this [`Socks5Acceptor`],
131    /// used to accept incoming [`Command::Connect`] [`client::Request`]s.
132    ///
133    /// Use [`Socks5Acceptor::with_default_connector`] in case
134    /// the [`DefaultConnector`] serves your needs just fine.
135    pub fn with_connector<C>(self, connector: C) -> Socks5Acceptor<C, B, U, A> {
136        Socks5Acceptor {
137            connector,
138            binder: self.binder,
139            udp_associator: self.udp_associator,
140            auth: self.auth,
141            auth_opt: self.auth_opt,
142            exec: self.exec,
143        }
144    }
145
146    /// Attach the [`DefaultConnector`] to this [`Socks5Acceptor`],
147    /// used to accept incoming [`Command::Connect`] [`client::Request`]s.
148    ///
149    /// Use [`Socks5Acceptor::with_connector`] in case you want to use a custom
150    /// [`Socks5Connector`] or customised [`Connector`].
151    #[inline]
152    pub fn with_default_connector(self) -> Socks5Acceptor<DefaultConnector, B, U, A> {
153        self.with_connector(DefaultConnector::default())
154    }
155}
156
157impl<C, U, A> Socks5Acceptor<C, (), U, A> {
158    /// Attach a [`Socks5Binder`] to this [`Socks5Acceptor`],
159    /// used to accept incoming [`Command::Bind`] [`client::Request`]s.
160    ///
161    /// Use [`Socks5Acceptor::with_default_binder`] in case
162    /// the [`DefaultBinder`] serves your needs just fine.
163    pub fn with_binder<B>(self, binder: B) -> Socks5Acceptor<C, B, U, A> {
164        Socks5Acceptor {
165            connector: self.connector,
166            binder,
167            udp_associator: self.udp_associator,
168            auth: self.auth,
169            auth_opt: self.auth_opt,
170            exec: self.exec,
171        }
172    }
173
174    /// Attach the [`DefaultBinder`] to this [`Socks5Acceptor`],
175    /// used to accept incoming [`Command::Bind`] [`client::Request`]s.
176    ///
177    /// Use [`Socks5Acceptor::with_binder`] in case you want to use a custom
178    /// [`Socks5Binder`] or customised [`Binder`].
179    #[inline]
180    pub fn with_default_binder(self) -> Socks5Acceptor<C, DefaultBinder, U, A> {
181        self.with_binder(DefaultBinder::default())
182    }
183}
184
185impl<C, B, A> Socks5Acceptor<C, B, (), A> {
186    /// Attach a [`Socks5UdpAssociator`] to this [`Socks5Acceptor`],
187    /// used to accept incoming [`Command::UdpAssociate`] [`client::Request`]s.
188    ///
189    /// Use [`Socks5Acceptor::with_default_udp_associator`] in case
190    /// the [`DefaultUdpRelay`] serves your needs just fine.
191    pub fn with_udp_associator<U>(self, udp_associator: U) -> Socks5Acceptor<C, B, U, A> {
192        Socks5Acceptor {
193            connector: self.connector,
194            binder: self.binder,
195            udp_associator,
196            auth: self.auth,
197            auth_opt: self.auth_opt,
198            exec: self.exec,
199        }
200    }
201
202    /// Attach the [`DefaultUdpRelay`] to this [`Socks5Acceptor`],
203    /// used to accept incoming [`Command::UdpAssociate`] [`client::Request`]s.
204    ///
205    /// Use [`Socks5Acceptor::with_udp_associator`] in case you want to use a custom
206    /// [`Socks5UdpAssociator`] or customised [`udp::UdpRelay`].
207    #[inline]
208    pub fn with_default_udp_associator(self) -> Socks5Acceptor<C, B, DefaultUdpRelay, A> {
209        self.with_udp_associator(DefaultUdpRelay::default())
210    }
211}
212
213impl Socks5Acceptor {
214    #[inline]
215    pub fn default_with_executor(exec: Executor) -> Self {
216        Socks5Acceptor::new(exec).with_default_connector()
217    }
218}
219
220impl Default for Socks5Acceptor {
221    #[inline]
222    fn default() -> Self {
223        Self::default_with_executor(Executor::default())
224    }
225}
226
227#[derive(Debug)]
228/// Server-side error returned in case of a failure during the handshake process.
229pub struct Error {
230    kind: ErrorKind,
231    context: ErrorContext,
232    source: Option<BoxError>,
233}
234
235#[derive(Debug)]
236enum ErrorContext {
237    None,
238    Message(&'static str),
239    ReplyKind(ReplyKind),
240}
241
242impl From<&'static str> for ErrorContext {
243    fn from(value: &'static str) -> Self {
244        Self::Message(value)
245    }
246}
247
248impl From<ReplyKind> for ErrorContext {
249    fn from(value: ReplyKind) -> Self {
250        Self::ReplyKind(value)
251    }
252}
253
254impl Error {
255    fn io(err: std::io::Error) -> Self {
256        Self {
257            kind: ErrorKind::IO,
258            context: ErrorContext::None,
259            source: Some(err.into()),
260        }
261    }
262
263    fn protocol(err: ProtocolError) -> Self {
264        Self {
265            kind: ErrorKind::Protocol,
266            context: ErrorContext::None,
267            source: Some(err.into()),
268        }
269    }
270
271    fn aborted(reason: &'static str) -> Self {
272        Self {
273            kind: ErrorKind::Aborted(reason),
274            context: ErrorContext::None,
275            source: None,
276        }
277    }
278
279    fn service(error: impl Into<BoxError>) -> Self {
280        Self {
281            kind: ErrorKind::Service,
282            context: ErrorContext::None,
283            source: Some(error.into()),
284        }
285    }
286
287    fn with_context(mut self, context: impl Into<ErrorContext>) -> Self {
288        self.context = context.into();
289        self
290    }
291
292    fn with_source(mut self, err: impl Into<BoxError>) -> Self {
293        self.source = Some(err.into());
294        self
295    }
296}
297
298#[derive(Debug)]
299enum ErrorKind {
300    IO,
301    Protocol,
302    Aborted(&'static str),
303    Service,
304}
305
306impl fmt::Display for ErrorContext {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        match self {
309            Self::Message(message) => write!(f, "{message}"),
310            Self::ReplyKind(kind) => write!(f, "reply: {kind}"),
311            Self::None => write!(f, "no context"),
312        }
313    }
314}
315
316impl fmt::Display for Error {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        let context = &self.context;
319        match &self.kind {
320            ErrorKind::IO => {
321                write!(f, "server: handshake error: I/O ({context})")
322            }
323            ErrorKind::Protocol => {
324                write!(f, "server: handshake error: protocol error ({context})")
325            }
326            ErrorKind::Aborted(reason) => {
327                write!(f, "server: handshake error: aborted: {reason} ({context})")
328            }
329            ErrorKind::Service => {
330                write!(f, "server: service error ({context})")
331            }
332        }
333    }
334}
335
336impl std::error::Error for Error {
337    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
338        self.source.as_ref().and_then(|e| e.source())
339    }
340}
341
342impl<C, B, U, A> Socks5Acceptor<C, B, U, A> {
343    pub async fn accept<S>(&self, mut stream: S) -> Result<(), Error>
344    where
345        C: Socks5Connector<S>,
346        U: Socks5UdpAssociator<S>,
347        A: Authorizer<user::Basic, Error: fmt::Debug>,
348        B: Socks5Binder<S>,
349        S: Io + Unpin + ExtensionsRef,
350    {
351        let client_header = client::Header::read_from(&mut stream)
352            .await
353            .map_err(|err| Error::protocol(err).with_context("read client header"))?;
354
355        let (negotiated_method, maybe_ext) = self
356            .handle_method(&client_header.methods, &mut stream)
357            .await?;
358
359        if let Some(ext) = maybe_ext {
360            stream.extensions().extend(&ext);
361        }
362
363        tracing::trace!(
364            "socks5 server: headers exchanged negotiated method = {negotiated_method:?} (for client methods: {:?}",
365            client_header.methods,
366        );
367
368        let client_request = client::Request::read_from(&mut stream)
369            .await
370            .map_err(|err| Error::protocol(err).with_context("read client request"))?;
371        tracing::trace!(
372            "socks5 server w/ destination {} and negotiated method {:?} (for client methods: {:?}): client request received cmd {:?}",
373            client_request.destination,
374            negotiated_method,
375            client_header.methods,
376            client_request.command,
377        );
378
379        stream.extensions().insert(StreamTransformed {
380            by: "rama-socks5::Socks5Acceptor",
381        });
382
383        match client_request.command {
384            Command::Connect => {
385                self.connector
386                    .accept_connect(stream, client_request.destination)
387                    .await
388            }
389            Command::Bind => {
390                self.binder
391                    .accept_bind(stream, client_request.destination)
392                    .await
393            }
394            Command::UdpAssociate => {
395                self.udp_associator
396                    .accept_udp_associate(stream, client_request.destination)
397                    .await
398            }
399            Command::Unknown(_) => {
400                tracing::debug!(
401                    "socks5 server w/ destination {} for negotiated method: {:?} (for client methods: {:?}): abort: unknown command {:?} not supported",
402                    client_request.destination,
403                    negotiated_method,
404                    client_header.methods,
405                    client_request.command,
406                );
407
408                Reply::error_reply(ReplyKind::CommandNotSupported)
409                    .write_to(&mut stream)
410                    .await
411                    .map_err(|err| {
412                        Error::io(err)
413                            .with_context("write server reply: unknown command not supported")
414                    })?;
415                Err(Error::aborted("unknown command not supported")
416                    .with_context(ReplyKind::CommandNotSupported))
417            }
418        }
419    }
420}
421
422impl<C, B, U, A: Authorizer<user::Basic, Error: fmt::Debug>> Socks5Acceptor<C, B, U, A> {
423    async fn handle_method<S: Io + Unpin>(
424        &self,
425        methods: &[SocksMethod],
426        stream: &mut S,
427    ) -> Result<(SocksMethod, Option<Extensions>), Error> {
428        match &self.auth {
429            AuthKind::WithAuth(authorizer) => {
430                if methods.contains(&SocksMethod::UsernamePassword) {
431                    Header::new(SocksMethod::UsernamePassword)
432                        .write_to(stream)
433                        .await
434                        .map_err(|err| {
435                            Error::io(err)
436                                .with_context("write server reply: auth (username-password)")
437                        })?;
438
439                    let client_auth_req = client::UsernamePasswordRequest::read_from(stream)
440                        .await
441                        .map_err(|err| {
442                            Error::protocol(err).with_context(
443                                "read client auth sub-negotiation request: username-password",
444                            )
445                        })?;
446                    let user::authority::AuthorizeResult { result, .. } =
447                        authorizer.authorize(client_auth_req.basic).await;
448                    match result {
449                        Ok(maybe_ext) => {
450                            UsernamePasswordResponse::new_success()
451                                .write_to(stream)
452                                .await
453                                .map_err(|err| {
454                                    Error::io(err).with_context(
455                                        "write server auth sub-negotiation success response",
456                                    )
457                                })?;
458                            Ok((SocksMethod::UsernamePassword, maybe_ext))
459                        }
460                        Err(err) => {
461                            tracing::trace!(
462                                "socks5 acceptor's authorizer stopped inc request: {err:?}"
463                            );
464                            UsernamePasswordResponse::new_invalid_credentails()
465                                .write_to(stream)
466                                .await
467                                .map_err(|err| {
468                                    Error::io(err).with_context(
469                                    "write server auth sub-negotiation error response: unauthorized",
470                                )
471                                })?;
472                            Err(Error::aborted("username-password: client unauthorized"))
473                        }
474                    }
475                } else if self.auth_opt && methods.contains(&SocksMethod::NoAuthenticationRequired)
476                {
477                    tracing::trace!(
478                        "socks5 server: auth supported but optional: skipping auth as client does not support username-passowrd auth",
479                    );
480
481                    Header::new(SocksMethod::NoAuthenticationRequired)
482                        .write_to(stream)
483                        .await
484                        .map_err(|err| {
485                            Error::io(err).with_context("write server reply: no auth required")
486                        })?;
487
488                    Ok((SocksMethod::NoAuthenticationRequired, None))
489                } else {
490                    Header::new(SocksMethod::NoAcceptableMethods)
491                        .write_to(stream)
492                        .await
493                        .map_err(|err| {
494                            Error::io(err).with_context(
495                        "write server auth sub-negotiation error response: no acceptable methods",
496                    )
497                        })?;
498                    Err(Error::aborted(
499                        "username-password required but client doesn't support the method (auth == required)",
500                    ))
501                }
502            }
503            AuthKind::NoAuth(_) => {
504                if methods.contains(&SocksMethod::NoAuthenticationRequired) {
505                    Header::new(SocksMethod::NoAuthenticationRequired)
506                        .write_to(stream)
507                        .await
508                        .map_err(|err| {
509                            Error::io(err).with_context("write server reply: no auth required")
510                        })?;
511
512                    return Ok((SocksMethod::NoAuthenticationRequired, None));
513                }
514
515                Header::new(SocksMethod::NoAcceptableMethods)
516                    .write_to(stream)
517                    .await
518                    .map_err(|err| {
519                        Error::io(err).with_context(
520                    "write server auth sub-negotiation error response: no acceptable methods",
521                )
522                    })?;
523                Err(Error::aborted("no acceptable methods"))
524            }
525        }
526    }
527}
528
529impl<C, B, U, A, S> Service<S> for Socks5Acceptor<C, B, U, A>
530where
531    C: Socks5Connector<S>,
532    U: Socks5UdpAssociator<S>,
533    A: Authorizer<user::Basic, Error: fmt::Debug>,
534    B: Socks5Binder<S>,
535    S: Io + Unpin + ExtensionsRef,
536{
537    type Output = ();
538    type Error = Error;
539
540    #[inline]
541    fn serve(
542        &self,
543        stream: S,
544    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + '_ {
545        self.accept(stream)
546    }
547}
548
549impl<C, B, U, A> Socks5Acceptor<C, B, U, A>
550where
551    C: Socks5Connector<TcpStream>,
552    U: Socks5UdpAssociator<TcpStream>,
553    A: Authorizer<user::Basic, Error: fmt::Debug>,
554    B: Socks5Binder<TcpStream>,
555{
556    /// Listen for connections on the given [`SocketAddress`], serving Socks5(h) connections.
557    ///
558    /// It's a shortcut in case you don't need to operate on the transport layer directly.
559    pub async fn listen<Address>(self, address: Address) -> Result<(), BoxError>
560    where
561        Address: TryInto<SocketAddress, Error: Into<BoxError>>,
562    {
563        let tcp = TcpListener::bind_address(address, self.exec.clone()).await?;
564        tcp.serve(Arc::new(self)).await;
565        Ok(())
566    }
567}
568
569#[cfg(test)]
570mod test;