Skip to main content

eggress_protocol_reverse/
compat_pproxy.rs

1//! The deliberately small wire adapter for pproxy 2.7.9 backward links.
2//!
3//! pproxy's ProxyBackward is not the native Eggress reverse protocol. A
4//! worker opens a transport to the remote listener, writes the configured auth
5//! bytes without a delimiter, and then waits for the remote listener to put
6//! that channel to work. Keeping this implementation separate is important:
7//! changing the native reverse handshake would change the native protocol.
8
9use crate::client::TargetResolver;
10use crate::{relay_bidirectional_with_timeout, ProtocolError};
11use eggress_core::{TargetAddr, TargetHost};
12use eggress_uri::{ProtocolSpec, ProxyChainSpec};
13use std::net::SocketAddr;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::io::{AsyncReadExt, AsyncWriteExt};
17use tokio::net::{TcpListener, TcpStream};
18use tokio::sync::mpsc;
19use tokio::task::JoinSet;
20use tokio_util::sync::CancellationToken;
21use tracing::{debug, info, warn};
22
23/// pproxy backward reconnect states, exposed for deterministic tests and
24/// diagnostics.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PproxyBackwardState {
27    Disconnected,
28    Connecting,
29    Authenticating,
30    ReadyChannel,
31    Retrying,
32    Closed,
33}
34
35/// Channel framing applied between the compatibility adapter and the
36/// peer `pproxy 2.7.9` process after the auth handshake completes.
37///
38/// pproxy's `+in` worker runs the SOCKS5 server side after auth, expecting
39/// the listener side to send a SOCKS5 hello. The `Raw` framing keeps the
40/// older byte-pipe model for Eggress-internal use.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum PproxyBackwardFraming {
43    /// Bytes flow between the queued channel and the configured target with
44    /// no protocol framing. Used for Eggress-internal reverse tests where
45    /// the channel is paired with a plain TCP external client.
46    #[default]
47    Raw,
48    /// SOCKS5 server (worker side) or client (listener side) framing matches
49    /// pproxy 2.7.9 `+in` semantics so payload-level interop with the real
50    /// pproxy interpreter can be verified byte-for-byte.
51    Socks5,
52}
53
54/// Configuration for one pproxy backward worker.
55#[derive(Debug, Clone)]
56pub struct PproxyBackwardClientConfig {
57    pub server_addr: SocketAddr,
58    /// Full pproxy chain. Hop zero is the raw backward endpoint; later hops
59    /// are used as transport jumps in reverse order.
60    pub server_chain: Option<ProxyChainSpec>,
61    /// Raw pproxy auth bytes. This is intentionally not newline terminated.
62    pub auth: Vec<u8>,
63    pub reconnect_initial_ms: u64,
64    pub reconnect_max_ms: u64,
65    pub read_timeout_ms: u64,
66    pub target_connect_timeout_ms: u64,
67    /// Channel framing used after the auth handshake. Defaults to `Raw`.
68    pub server_framing: PproxyBackwardFraming,
69}
70
71impl Default for PproxyBackwardClientConfig {
72    fn default() -> Self {
73        Self {
74            server_addr: "127.0.0.1:0".parse().expect("valid default socket address"),
75            server_chain: None,
76            auth: Vec::new(),
77            reconnect_initial_ms: 100,
78            reconnect_max_ms: 30_000,
79            read_timeout_ms: 60_000,
80            target_connect_timeout_ms: 10_000,
81            server_framing: PproxyBackwardFraming::default(),
82        }
83    }
84}
85
86/// A pproxy-compatible backward client. One instance owns exactly one
87/// persistent worker; callers create one instance per +in occurrence.
88pub struct PproxyBackwardClient {
89    config: PproxyBackwardClientConfig,
90    cancel: CancellationToken,
91    resolver: Arc<dyn TargetResolver>,
92}
93
94impl PproxyBackwardClient {
95    pub fn new(config: PproxyBackwardClientConfig, resolver: Arc<dyn TargetResolver>) -> Self {
96        Self {
97            config,
98            cancel: CancellationToken::new(),
99            resolver,
100        }
101    }
102
103    pub fn cancel_token(&self) -> CancellationToken {
104        self.cancel.clone()
105    }
106
107    pub async fn run(&self) -> Result<(), ProtocolError> {
108        let mut backoff = self.config.reconnect_initial_ms.max(1);
109        loop {
110            if self.cancel.is_cancelled() {
111                break;
112            }
113
114            match self.run_connection().await {
115                Ok(()) => backoff = self.config.reconnect_initial_ms.max(1),
116                Err(error) => {
117                    if self.cancel.is_cancelled() {
118                        break;
119                    }
120                    warn!(error = %error, backoff_ms = backoff, "pproxy backward channel failed");
121                    let delay = tokio::time::sleep(Duration::from_millis(backoff));
122                    tokio::pin!(delay);
123                    tokio::select! {
124                        _ = &mut delay => {}
125                        _ = self.cancel.cancelled() => break,
126                    }
127                    backoff = backoff
128                        .saturating_mul(2)
129                        .min(self.config.reconnect_max_ms.max(1));
130                }
131            }
132        }
133        Ok(())
134    }
135
136    async fn run_connection(&self) -> Result<(), ProtocolError> {
137        let mut stream = self.connect_control().await?;
138        if !self.config.auth.is_empty() {
139            // Observable pproxy framing: raw bytes, no newline, and no native
140            // Eggress accept/reject byte.
141            stream.write_all(&self.config.auth).await?;
142            stream.flush().await?;
143        }
144
145        match self.config.server_framing {
146            PproxyBackwardFraming::Raw => self.run_connection_raw(stream).await,
147            PproxyBackwardFraming::Socks5 => self.run_connection_socks5(stream).await,
148        }
149    }
150
151    /// Raw byte-pipe mode: connect to the configured resolver target and
152    /// relay bytes between the channel and that target. Used by
153    /// Eggress-internal reverse tests where the external side is a plain
154    /// TCP client.
155    async fn run_connection_raw(&self, stream: TcpStream) -> Result<(), ProtocolError> {
156        let (host, port) = match self.resolver.resolve() {
157            crate::client::TargetResolution::Connect { host, port } => (host, port),
158            crate::client::TargetResolution::Reject { reason } => {
159                return Err(ProtocolError::ConfigInvalid(format!(
160                    "pproxy backward route rejected: {reason}"
161                )))
162            }
163        };
164
165        let timeout = Duration::from_millis(self.config.target_connect_timeout_ms.max(1));
166        let target = tokio::time::timeout(timeout, TcpStream::connect((host.as_str(), port)))
167            .await
168            .map_err(|_| {
169                ProtocolError::Io(std::io::Error::new(
170                    std::io::ErrorKind::TimedOut,
171                    "pproxy backward target connect timed out",
172                ))
173            })??;
174
175        relay_bidirectional_with_timeout(
176            stream,
177            target,
178            (self.config.read_timeout_ms > 0)
179                .then(|| Duration::from_millis(self.config.read_timeout_ms)),
180        )
181        .await
182    }
183
184    /// SOCKS5 server mode. pproxy 2.7.9 `+in` workers run the SOCKS5 server
185    /// side after the auth handshake, sending `[0x05, n, ...methods]` and
186    /// expecting a methods selection back. Reading the resulting CONNECT
187    /// target gives this worker the local echo target, matching the byte
188    /// payload relayed end-to-end through the real pproxy interpreter.
189    async fn run_connection_socks5(&self, mut stream: TcpStream) -> Result<(), ProtocolError> {
190        const SOCKS5_VERSION: u8 = 0x05;
191        const SOCKS5_METHOD_NONE: u8 = 0x00;
192        const SOCKS5_CMD_CONNECT: u8 = 0x01;
193        const SOCKS5_RSV: u8 = 0x00;
194        const SOCKS5_ATYP_IPV4: u8 = 0x01;
195        const SOCKS5_ATYP_DOMAIN: u8 = 0x03;
196        const SOCKS5_ATYP_IPV6: u8 = 0x04;
197        const SOCKS5_REP_SUCCESS: u8 = 0x00;
198
199        // SOCKS5 hello: [version, nmethods, methods...]
200        let mut header = [0u8; 2];
201        stream.read_exact(&mut header).await?;
202        if header[0] != SOCKS5_VERSION {
203            return Err(ProtocolError::ConfigInvalid(format!(
204                "pproxy backward SOCKS5 hello version mismatch: {}",
205                header[0]
206            )));
207        }
208        let nmethods = header[1] as usize;
209        let mut methods = vec![0u8; nmethods];
210        if nmethods > 0 {
211            stream.read_exact(&mut methods).await?;
212        }
213        if !methods.contains(&SOCKS5_METHOD_NONE) {
214            stream.write_all(&[SOCKS5_VERSION, 0xff]).await?;
215            stream.flush().await?;
216            return Err(ProtocolError::AuthFailed);
217        }
218        stream
219            .write_all(&[SOCKS5_VERSION, SOCKS5_METHOD_NONE])
220            .await?;
221        stream.flush().await?;
222
223        // SOCKS5 CONNECT request: [version, cmd, rsv, atyp, ...]
224        let mut req_header = [0u8; 4];
225        stream.read_exact(&mut req_header).await?;
226        if req_header[0] != SOCKS5_VERSION || req_header[1] != SOCKS5_CMD_CONNECT {
227            return Err(ProtocolError::ConfigInvalid(format!(
228                "pproxy backward SOCKS5 request header invalid: {:?}",
229                &req_header[..]
230            )));
231        }
232        let _rsv = req_header[2];
233        if req_header[2] != SOCKS5_RSV {
234            return Err(ProtocolError::ConfigInvalid(format!(
235                "pproxy backward SOCKS5 RSV must be zero, got {}",
236                req_header[2]
237            )));
238        }
239        let atyp = req_header[3];
240        let host = match atyp {
241            SOCKS5_ATYP_IPV4 => {
242                let mut addr = [0u8; 4];
243                stream.read_exact(&mut addr).await?;
244                std::net::IpAddr::V4(std::net::Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]))
245                    .to_string()
246            }
247            SOCKS5_ATYP_DOMAIN => {
248                let mut len = [0u8; 1];
249                stream.read_exact(&mut len).await?;
250                let n = len[0] as usize;
251                if n == 0 {
252                    return Err(ProtocolError::ConfigInvalid(
253                        "pproxy backward SOCKS5 domain length zero".into(),
254                    ));
255                }
256                let mut domain = vec![0u8; n];
257                stream.read_exact(&mut domain).await?;
258                String::from_utf8(domain)
259                    .map_err(|_| ProtocolError::ConfigInvalid("invalid SOCKS5 domain".into()))?
260            }
261            SOCKS5_ATYP_IPV6 => {
262                let mut addr = [0u8; 16];
263                stream.read_exact(&mut addr).await?;
264                std::net::IpAddr::V6(std::net::Ipv6Addr::from(addr)).to_string()
265            }
266            _ => {
267                stream
268                    .write_all(&[
269                        SOCKS5_VERSION,
270                        0x08,
271                        SOCKS5_RSV,
272                        SOCKS5_ATYP_IPV4,
273                        0,
274                        0,
275                        0,
276                        0,
277                        0,
278                        0,
279                    ])
280                    .await?;
281                stream.flush().await?;
282                return Err(ProtocolError::ConfigInvalid(format!(
283                    "pproxy backward SOCKS5 ATYP {atyp} unsupported"
284                )));
285            }
286        };
287        let mut port_bytes = [0u8; 2];
288        stream.read_exact(&mut port_bytes).await?;
289        let port = u16::from_be_bytes(port_bytes);
290
291        let timeout = Duration::from_millis(self.config.target_connect_timeout_ms.max(1));
292        let target = tokio::time::timeout(timeout, TcpStream::connect((host.as_str(), port)))
293            .await
294            .map_err(|_| {
295                ProtocolError::Io(std::io::Error::new(
296                    std::io::ErrorKind::TimedOut,
297                    "pproxy backward SOCKS5 target connect timed out",
298                ))
299            })?;
300        let target = match target {
301            Ok(t) => t,
302            Err(error) => {
303                // Reply with a connection refused-style SOCKS5 response so
304                // the peer closes the channel cleanly.
305                let _ = stream
306                    .write_all(&[
307                        SOCKS5_VERSION,
308                        0x05,
309                        SOCKS5_RSV,
310                        SOCKS5_ATYP_IPV4,
311                        0,
312                        0,
313                        0,
314                        0,
315                        0,
316                        0,
317                    ])
318                    .await;
319                let _ = stream.flush().await;
320                return Err(ProtocolError::Io(error));
321            }
322        };
323
324        stream
325            .write_all(&[
326                SOCKS5_VERSION,
327                SOCKS5_REP_SUCCESS,
328                SOCKS5_RSV,
329                SOCKS5_ATYP_IPV4,
330                0,
331                0,
332                0,
333                0,
334                0,
335                0,
336            ])
337            .await?;
338        stream.flush().await?;
339
340        relay_bidirectional_with_timeout(
341            stream,
342            target,
343            (self.config.read_timeout_ms > 0)
344                .then(|| Duration::from_millis(self.config.read_timeout_ms)),
345        )
346        .await
347    }
348
349    async fn connect_control(&self) -> Result<TcpStream, ProtocolError> {
350        let Some(chain) = self.config.server_chain.as_ref() else {
351            return Ok(TcpStream::connect(self.config.server_addr).await?);
352        };
353        if chain.hops.len() <= 1 {
354            return Ok(TcpStream::connect(self.config.server_addr).await?);
355        }
356        if chain.hops.iter().any(|hop| hop.tls) {
357            return Err(ProtocolError::ConfigInvalid(
358                "TLS-wrapped pproxy backward jumps require a configured TLS transport".into(),
359            ));
360        }
361
362        let last = chain.hops.last().expect("chain length checked");
363        let mut stream =
364            TcpStream::connect((last.endpoint.host.as_str(), last.endpoint.port)).await?;
365        for index in (1..chain.hops.len()).rev() {
366            let jump = &chain.hops[index];
367            let endpoint = &chain.hops[index - 1].endpoint;
368            let target = TargetAddr {
369                host: endpoint
370                    .host
371                    .parse()
372                    .map(TargetHost::Ip)
373                    .unwrap_or_else(|_| TargetHost::Domain(endpoint.host.clone())),
374                port: endpoint.port,
375            };
376            stream = connect_jump(stream, jump, &target).await?;
377        }
378        Ok(stream)
379    }
380
381    pub fn shutdown(&self) {
382        self.cancel.cancel();
383    }
384}
385
386/// Configuration for the pproxy-compatible accepting side.
387#[derive(Debug, Clone)]
388pub struct PproxyBackwardServerConfig {
389    pub control_bind: SocketAddr,
390    pub external_bind: SocketAddr,
391    /// Raw auth bytes expected immediately after the transport is accepted.
392    pub auth: Vec<u8>,
393    pub max_control_connections: usize,
394    pub max_pending_external: usize,
395    pub read_timeout_ms: u64,
396    /// Optional fixed target the server forwards each external client to
397    /// after the SOCKS5 CONNECT handshake. The SOCKS5 framing negotiates a
398    /// destination with the pproxy worker side and uses it for the channel.
399    pub socks5_target: Option<(String, u16)>,
400    /// Channel framing applied between the listener and pproxy worker
401    /// channels. The `Raw` framing keeps the older byte-pipe model for
402    /// Eggress-internal reverse tests; `Socks5` matches pproxy 2.7.9 `+in`
403    /// so payload-level interop with the real pproxy interpreter can be
404    /// verified byte-for-byte.
405    pub client_framing: PproxyBackwardFraming,
406}
407
408impl Default for PproxyBackwardServerConfig {
409    fn default() -> Self {
410        Self {
411            control_bind: "127.0.0.1:0".parse().expect("valid default socket address"),
412            external_bind: "127.0.0.1:0".parse().expect("valid default socket address"),
413            auth: Vec::new(),
414            max_control_connections: 256,
415            max_pending_external: 1024,
416            read_timeout_ms: 300_000,
417            socks5_target: None,
418            client_framing: PproxyBackwardFraming::default(),
419        }
420    }
421}
422
423struct QueuedChannel {
424    stream: TcpStream,
425}
426
427/// pproxy-compatible accepting side. It has no native handshake byte and
428/// never mutates the native ReverseServer.
429pub struct PproxyBackwardServer {
430    config: PproxyBackwardServerConfig,
431    cancel: CancellationToken,
432}
433
434impl PproxyBackwardServer {
435    pub fn new(config: PproxyBackwardServerConfig) -> Self {
436        Self {
437            config,
438            cancel: CancellationToken::new(),
439        }
440    }
441
442    pub fn cancel_token(&self) -> CancellationToken {
443        self.cancel.clone()
444    }
445
446    pub async fn run(self) -> Result<(), ProtocolError> {
447        let control_listener = TcpListener::bind(self.config.control_bind).await?;
448        let external_listener = TcpListener::bind(self.config.external_bind).await?;
449        let (control_tx, mut control_rx) =
450            mpsc::channel::<QueuedChannel>(self.config.max_control_connections.max(1));
451        let cancel = self.cancel.clone();
452        let config = Arc::new(self.config);
453        let mut tasks = JoinSet::new();
454
455        let accept_cancel = cancel.clone();
456        let accept_config = config.clone();
457        tasks.spawn(async move {
458            loop {
459                tokio::select! {
460                    result = control_listener.accept() => {
461                        let (mut stream, peer) = match result {
462                            Ok(value) => value,
463                            Err(error) => {
464                                warn!(%error, "pproxy backward control accept failed");
465                                continue;
466                            }
467                        };
468                        let auth = accept_config.auth.clone();
469                        let tx = control_tx.clone();
470                        let timeout = accept_config.read_timeout_ms;
471                        let framing = accept_config.client_framing;
472                        let socks5_target = accept_config.socks5_target.clone();
473                        tokio::spawn(async move {
474                            if !auth.is_empty() {
475                                let mut received = vec![0u8; auth.len()];
476                                let read = tokio::time::timeout(
477                                    Duration::from_millis(timeout.max(1)),
478                                    stream.read_exact(&mut received),
479                                ).await;
480                                if !matches!(read, Ok(Ok(_))) || received != auth {
481                                    debug!(%peer, "pproxy backward auth rejected");
482                                    return;
483                                }
484                            }
485                            if matches!(framing, PproxyBackwardFraming::Socks5) {
486                                if let Err(error) = proxy_socks5_setup(&mut stream).await {
487                                    debug!(%peer, %error, "pproxy backward SOCKS5 setup failed");
488                                    return;
489                                }
490                                if let Some((host, port)) = socks5_target {
491                                    if let Err(error) =
492                                        reply_socks5_connect(&mut stream, &host, port).await
493                                    {
494                                        debug!(
495                                            %peer,
496                                            %error,
497                                            "pproxy backward SOCKS5 CONNECT reply failed"
498                                        );
499                                        return;
500                                    }
501                                    // The worker dials the target and sends a
502                                    // SOCKS5 CONNECT reply. Drain it here so the
503                                    // channel only carries application bytes
504                                    // once it is paired with an external client.
505                                    if let Err(error) =
506                                        read_socks5_connect_reply(&mut stream).await
507                                    {
508                                        debug!(
509                                            %peer,
510                                            %error,
511                                            "pproxy backward SOCKS5 CONNECT reply read failed"
512                                        );
513                                        return;
514                                    }
515                                }
516                            }
517                            let _ = tx.send(QueuedChannel { stream }).await;
518                        });
519                    }
520                    _ = accept_cancel.cancelled() => break,
521                }
522            }
523        });
524
525        let external_cancel = cancel.clone();
526        let external_framing = config.client_framing;
527        let external_target = config.socks5_target.clone();
528        tasks.spawn(async move {
529            let mut relays = JoinSet::new();
530            loop {
531                tokio::select! {
532                    result = external_listener.accept() => {
533                        let (external, peer) = match result {
534                            Ok(value) => value,
535                            Err(error) => {
536                                warn!(%error, "pproxy backward external accept failed");
537                                continue;
538                            }
539                        };
540                        let control = tokio::select! {
541                            value = control_rx.recv() => value,
542                            _ = external_cancel.cancelled() => break,
543                        };
544                        let Some(control) = control else { break };
545                        let timeout = config.read_timeout_ms;
546                        let target = external_target.clone();
547                        relays.spawn(async move {
548                            debug!(%peer, "relaying pproxy backward channel");
549                            let result = relay_pproxy_pair(
550                                external,
551                                control.stream,
552                                target,
553                                external_framing,
554                                timeout,
555                            )
556                            .await;
557                            if let Err(error) = result {
558                                debug!(%peer, %error, "pproxy backward relay finished with error");
559                            }
560                        });
561                    }
562                    _ = external_cancel.cancelled() => break,
563                }
564            }
565            relays.abort_all();
566            while relays.join_next().await.is_some() {}
567        });
568
569        cancel.cancelled().await;
570        tasks.abort_all();
571        while tasks.join_next().await.is_some() {}
572        info!("pproxy backward server shut down");
573        Ok(())
574    }
575
576    pub fn shutdown(&self) {
577        self.cancel.cancel();
578    }
579}
580
581/// Build the raw auth field used by pproxy's ProxySimple.auth property.
582pub fn raw_auth(username: Option<&str>, password: Option<&str>) -> Vec<u8> {
583    match (username, password) {
584        (Some(user), Some(pass)) => format!("{user}:{pass}").into_bytes(),
585        (Some(user), None) => user.as_bytes().to_vec(),
586        (None, Some(pass)) => pass.as_bytes().to_vec(),
587        (None, None) => Vec::new(),
588    }
589}
590
591/// Drive the SOCKS5 hello + methods selection half of the channel handshake
592/// from the server side. pproxy 2.7.9 `+in` workers act as SOCKS5 servers
593/// after auth and wait for a SOCKS5 client hello. The listener (this side)
594/// acts as the SOCKS5 client, sends the hello, then waits for the worker
595/// to choose a method.
596async fn proxy_socks5_setup(stream: &mut TcpStream) -> Result<(), ProtocolError> {
597    stream.write_all(&[0x05, 0x01, 0x00]).await?;
598    stream.flush().await?;
599    let mut header = [0u8; 2];
600    stream.read_exact(&mut header).await?;
601    if header[0] != 0x05 {
602        return Err(ProtocolError::ConfigInvalid(format!(
603            "SOCKS5 methods selection version mismatch: {}",
604            header[0]
605        )));
606    }
607    if header[1] != 0x00 {
608        return Err(ProtocolError::AuthFailed);
609    }
610    Ok(())
611}
612
613/// Drain the SOCKS5 CONNECT reply from the worker after it has dialed the
614/// target. The reply header has the same shape as a SOCKS5 reply to a
615/// CONNECT request: `[version, rep, rsv, atyp, ...bound addr..., port]`.
616async fn read_socks5_connect_reply(stream: &mut TcpStream) -> Result<(), ProtocolError> {
617    let mut header = [0u8; 4];
618    stream.read_exact(&mut header).await?;
619    if header[0] != 0x05 {
620        return Err(ProtocolError::ConfigInvalid(format!(
621            "SOCKS5 CONNECT reply version mismatch: {}",
622            header[0]
623        )));
624    }
625    if header[1] != 0x00 {
626        return Err(ProtocolError::ConfigInvalid(format!(
627            "SOCKS5 CONNECT reply rep non-success: {}",
628            header[1]
629        )));
630    }
631    match header[3] {
632        0x01 => {
633            let mut tail = [0u8; 6];
634            stream.read_exact(&mut tail).await?;
635        }
636        0x04 => {
637            let mut tail = [0u8; 18];
638            stream.read_exact(&mut tail).await?;
639        }
640        0x03 => {
641            let mut len = [0u8; 1];
642            stream.read_exact(&mut len).await?;
643            let mut tail = vec![0u8; len[0] as usize + 2];
644            stream.read_exact(&mut tail).await?;
645        }
646        other => {
647            return Err(ProtocolError::ConfigInvalid(format!(
648                "SOCKS5 CONNECT reply ATYP {other} unsupported"
649            )));
650        }
651    }
652    Ok(())
653}
654
655/// Send a SOCKS5 CONNECT request through the channel to inform the worker
656/// of the destination it should reach. The worker dials that target via its
657/// `-r` upstream chain and relays bytes back through the channel.
658async fn reply_socks5_connect(
659    stream: &mut TcpStream,
660    host: &str,
661    port: u16,
662) -> Result<(), ProtocolError> {
663    // CONNECT request header
664    stream.write_all(&[0x05, 0x01, 0x00]).await?;
665    // ATYP + address
666    let parsed_host: std::net::IpAddr = host
667        .parse()
668        .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED));
669    if let std::net::IpAddr::V4(ipv4) = parsed_host {
670        stream.write_all(&[0x01]).await?;
671        stream.write_all(&ipv4.octets()).await?;
672    } else if let std::net::IpAddr::V6(ipv6) = parsed_host {
673        stream.write_all(&[0x04]).await?;
674        stream.write_all(&ipv6.octets()).await?;
675    } else {
676        let bytes = host.as_bytes();
677        if bytes.len() > 255 {
678            return Err(ProtocolError::ConfigInvalid(format!(
679                "SOCKS5 target host too long: {}",
680                bytes.len()
681            )));
682        }
683        stream.write_all(&[0x03, bytes.len() as u8]).await?;
684        stream.write_all(bytes).await?;
685    }
686    stream.write_all(&port.to_be_bytes()).await?;
687    stream.flush().await?;
688    Ok(())
689}
690
691/// Pair an external TCP client with a queued pproxy worker channel. Under
692/// the `Raw` framing this is a plain byte relay; under `Socks5` the
693/// listener already drove the channel CONNECT during the worker's
694/// initial handshake, so this only forwards the application bytes.
695async fn relay_pproxy_pair(
696    external: TcpStream,
697    control: TcpStream,
698    _target: Option<(String, u16)>,
699    framing: PproxyBackwardFraming,
700    timeout_ms: u64,
701) -> Result<(), ProtocolError> {
702    let timeout = (timeout_ms > 0).then(|| Duration::from_millis(timeout_ms));
703    match framing {
704        PproxyBackwardFraming::Raw => {
705            relay_bidirectional_with_timeout(external, control, timeout).await
706        }
707        PproxyBackwardFraming::Socks5 => {
708            relay_bidirectional_with_timeout(external, control, timeout).await
709        }
710    }
711}
712
713async fn connect_jump(
714    mut stream: TcpStream,
715    hop: &eggress_uri::ProxyHopSpec,
716    target: &TargetAddr,
717) -> Result<TcpStream, ProtocolError> {
718    match hop.protocols.as_slice() {
719        [ProtocolSpec::Http] | [ProtocolSpec::HttpOnly] => {
720            let authority = target.to_string();
721            let mut request = format!(
722                "CONNECT {authority} HTTP/1.1\r\nHost: {authority}\r\nConnection: keep-alive\r\n"
723            );
724            if let Some(credentials) = &hop.credentials {
725                let encoded = base64_encode(
726                    format!("{}:{}", credentials.username, credentials.password).as_bytes(),
727                );
728                request.push_str(&format!("Proxy-Authorization: Basic {encoded}\r\n"));
729            }
730            request.push_str("\r\n");
731            stream.write_all(request.as_bytes()).await?;
732            let mut response = Vec::new();
733            read_until_headers(&mut stream, &mut response).await?;
734            let status = response
735                .split(|byte| *byte == b' ')
736                .nth(1)
737                .and_then(|code| std::str::from_utf8(code).ok())
738                .and_then(|code| code.parse::<u16>().ok());
739            if status != Some(200) {
740                return Err(ProtocolError::ConfigInvalid(
741                    "pproxy backward HTTP jump rejected CONNECT".into(),
742                ));
743            }
744            Ok(stream)
745        }
746        [ProtocolSpec::Socks5] => {
747            let credentials = hop.credentials.as_ref();
748            if credentials.is_some() {
749                stream.write_all(&[5, 1, 2]).await?;
750            } else {
751                stream.write_all(&[5, 1, 0]).await?;
752            }
753            let mut method = [0u8; 2];
754            stream.read_exact(&mut method).await?;
755            if method[0] != 5 || method[1] == 0xff {
756                return Err(ProtocolError::ConfigInvalid(
757                    "pproxy backward SOCKS5 jump rejected authentication".into(),
758                ));
759            }
760            if method[1] == 2 {
761                let credentials = credentials.ok_or_else(|| {
762                    ProtocolError::ConfigInvalid(
763                        "SOCKS5 jump requested credentials that were not configured".into(),
764                    )
765                })?;
766                let user = credentials.username.as_bytes();
767                let pass = credentials.password.as_bytes();
768                if user.len() > 255 || pass.len() > 255 {
769                    return Err(ProtocolError::ConfigInvalid(
770                        "SOCKS5 jump credentials are too long".into(),
771                    ));
772                }
773                stream.write_all(&[1, user.len() as u8]).await?;
774                stream.write_all(user).await?;
775                stream.write_all(&[pass.len() as u8]).await?;
776                stream.write_all(pass).await?;
777                let mut auth_reply = [0u8; 2];
778                stream.read_exact(&mut auth_reply).await?;
779                if auth_reply != [1, 0] {
780                    return Err(ProtocolError::AuthFailed);
781                }
782            }
783            let address = encode_socks_address(target)?;
784            stream.write_all(&[5, 1, 0]).await?;
785            stream.write_all(&address).await?;
786            let mut reply = [0u8; 4];
787            stream.read_exact(&mut reply).await?;
788            if reply[1] != 0 {
789                return Err(ProtocolError::ConfigInvalid(format!(
790                    "SOCKS5 backward jump CONNECT failed with code {}",
791                    reply[1]
792                )));
793            }
794            let remaining = match reply[3] {
795                1 => 6,
796                4 => 18,
797                3 => {
798                    let mut length = [0u8; 1];
799                    stream.read_exact(&mut length).await?;
800                    usize::from(length[0]) + 2
801                }
802                _ => return Err(ProtocolError::ConfigInvalid("invalid SOCKS5 reply".into())),
803            };
804            let mut discard = vec![0u8; remaining];
805            stream.read_exact(&mut discard).await?;
806            Ok(stream)
807        }
808        _ => Err(ProtocolError::ConfigInvalid(
809            "pproxy backward jump supports only HTTP CONNECT and SOCKS5".into(),
810        )),
811    }
812}
813
814async fn read_until_headers(
815    stream: &mut TcpStream,
816    output: &mut Vec<u8>,
817) -> Result<(), ProtocolError> {
818    let mut byte = [0u8; 1];
819    while output.len() < 16 * 1024 {
820        stream.read_exact(&mut byte).await?;
821        output.push(byte[0]);
822        if output.ends_with(b"\r\n\r\n") {
823            return Ok(());
824        }
825    }
826    Err(ProtocolError::ConfigInvalid(
827        "proxy jump response headers exceed 16 KiB".into(),
828    ))
829}
830
831fn encode_socks_address(target: &TargetAddr) -> Result<Vec<u8>, ProtocolError> {
832    let mut output = Vec::new();
833    match &target.host {
834        TargetHost::Ip(std::net::IpAddr::V4(ip)) => {
835            output.push(1);
836            output.extend_from_slice(&ip.octets());
837        }
838        TargetHost::Ip(std::net::IpAddr::V6(ip)) => {
839            output.push(4);
840            output.extend_from_slice(&ip.octets());
841        }
842        TargetHost::Domain(domain) => {
843            if domain.len() > 255 {
844                return Err(ProtocolError::ConfigInvalid(
845                    "SOCKS5 backward jump target domain is too long".into(),
846                ));
847            }
848            output.push(3);
849            output.push(domain.len() as u8);
850            output.extend_from_slice(domain.as_bytes());
851        }
852    }
853    output.extend_from_slice(&target.port.to_be_bytes());
854    Ok(output)
855}
856
857fn base64_encode(input: &[u8]) -> String {
858    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
859    let mut output = String::new();
860    for chunk in input.chunks(3) {
861        let a = chunk[0];
862        let b = *chunk.get(1).unwrap_or(&0);
863        let c = *chunk.get(2).unwrap_or(&0);
864        output.push(TABLE[(a >> 2) as usize] as char);
865        output.push(TABLE[((a << 4 | b >> 4) & 0x3f) as usize] as char);
866        output.push(if chunk.len() > 1 {
867            TABLE[((b << 2 | c >> 6) & 0x3f) as usize] as char
868        } else {
869            '='
870        });
871        output.push(if chunk.len() > 2 {
872            TABLE[(c & 0x3f) as usize] as char
873        } else {
874            '='
875        });
876    }
877    output
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883    use crate::client::{TargetResolution, TargetResolver};
884
885    struct Resolver;
886    impl TargetResolver for Resolver {
887        fn resolve(&self) -> TargetResolution {
888            TargetResolution::Reject {
889                reason: "test".into(),
890            }
891        }
892    }
893
894    struct FixedResolver(SocketAddr);
895    impl TargetResolver for FixedResolver {
896        fn resolve(&self) -> TargetResolution {
897            TargetResolution::Connect {
898                host: self.0.ip().to_string(),
899                port: self.0.port(),
900            }
901        }
902    }
903
904    #[test]
905    fn raw_auth_is_not_newline_terminated() {
906        assert_eq!(raw_auth(Some("user"), Some("pass")), b"user:pass");
907        assert!(!raw_auth(Some("user"), Some("pass")).contains(&b'\n'));
908    }
909
910    #[tokio::test]
911    async fn client_cancellation_is_prompt() {
912        let client = PproxyBackwardClient::new(
913            PproxyBackwardClientConfig {
914                server_addr: "127.0.0.1:1".parse().unwrap(),
915                reconnect_initial_ms: 1,
916                reconnect_max_ms: 2,
917                ..Default::default()
918            },
919            Arc::new(Resolver),
920        );
921        let cancel = client.cancel_token();
922        let task = tokio::spawn(async move { client.run().await });
923        tokio::time::sleep(Duration::from_millis(5)).await;
924        cancel.cancel();
925        tokio::time::timeout(Duration::from_secs(1), task)
926            .await
927            .unwrap()
928            .unwrap()
929            .unwrap();
930    }
931
932    #[tokio::test]
933    async fn raw_backward_client_and_server_relay_without_native_handshake() {
934        let target_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
935        let target_addr = target_listener.local_addr().unwrap();
936        tokio::spawn(async move {
937            let (mut stream, _) = target_listener.accept().await.unwrap();
938            let mut buf = [0u8; 64];
939            let size = stream.read(&mut buf).await.unwrap();
940            stream.write_all(&buf[..size]).await.unwrap();
941        });
942
943        let control_addr = TcpListener::bind("127.0.0.1:0")
944            .await
945            .unwrap()
946            .local_addr()
947            .unwrap();
948        let external_addr = TcpListener::bind("127.0.0.1:0")
949            .await
950            .unwrap()
951            .local_addr()
952            .unwrap();
953        let server = PproxyBackwardServer::new(PproxyBackwardServerConfig {
954            control_bind: control_addr,
955            external_bind: external_addr,
956            auth: b"user:pass".to_vec(),
957            read_timeout_ms: 2_000,
958            ..Default::default()
959        });
960        let server_cancel = server.cancel_token();
961        let server_task = tokio::spawn(server.run());
962
963        let client = PproxyBackwardClient::new(
964            PproxyBackwardClientConfig {
965                server_addr: control_addr,
966                auth: b"user:pass".to_vec(),
967                reconnect_initial_ms: 1,
968                reconnect_max_ms: 5,
969                ..Default::default()
970            },
971            Arc::new(FixedResolver(target_addr)),
972        );
973        let client_cancel = client.cancel_token();
974        let client_task = tokio::spawn(async move { client.run().await });
975
976        let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
977        let mut external = loop {
978            match TcpStream::connect(external_addr).await {
979                Ok(stream) => break stream,
980                Err(error) if tokio::time::Instant::now() < deadline => {
981                    debug!(%error, "waiting for backward external listener in test");
982                    tokio::time::sleep(Duration::from_millis(5)).await;
983                }
984                Err(error) => panic!("backward external listener did not start: {error}"),
985            }
986        };
987        external.write_all(b"backward").await.unwrap();
988        let mut echoed = [0u8; 8];
989        tokio::time::timeout(Duration::from_secs(2), external.read_exact(&mut echoed))
990            .await
991            .unwrap()
992            .unwrap();
993        assert_eq!(&echoed, b"backward");
994
995        client_cancel.cancel();
996        server_cancel.cancel();
997        external.shutdown().await.unwrap();
998        tokio::time::timeout(Duration::from_secs(2), client_task)
999            .await
1000            .unwrap()
1001            .unwrap()
1002            .unwrap();
1003        tokio::time::timeout(Duration::from_secs(2), server_task)
1004            .await
1005            .unwrap()
1006            .unwrap()
1007            .unwrap();
1008    }
1009}