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