Skip to main content

proxychains_masq/
chain.rs

1use std::{
2    net::{IpAddr, Ipv4Addr, Ipv6Addr},
3    sync::atomic::{AtomicUsize, Ordering},
4    time::Duration,
5};
6
7use anyhow::{bail, Context, Result};
8use rand::{rngs::OsRng, seq::SliceRandom};
9use tokio::{net::TcpStream, time::timeout};
10use tracing::info;
11
12use crate::proxy::{http, https, raw, socks4, socks5, BoxStream, Target};
13
14// ─── Public types re-exported from config ─────────────────────────────────────
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ChainType {
18    Strict,
19    Dynamic,
20    Random,
21    RoundRobin,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ProxyType {
26    Socks4,
27    Socks5,
28    Http,
29    /// HTTP CONNECT over TLS; certificate validation is skipped so that
30    /// self-signed corporate proxy certs are accepted.
31    Https,
32    Raw,
33}
34
35/// A single proxy in the chain.
36#[derive(Debug, Clone)]
37pub struct ProxyEntry {
38    pub proxy_type: ProxyType,
39    pub addr: IpAddr,
40    pub port: u16,
41    pub username: Option<String>,
42    pub password: Option<String>,
43}
44
45/// A localnet exclusion rule.
46#[derive(Debug, Clone)]
47pub struct LocalNet {
48    pub addr: IpAddr,
49    pub mask_v4: Option<Ipv4Addr>,
50    pub prefix_v6: Option<u8>,
51    pub port: Option<u16>,
52}
53
54/// A DNAT rewrite rule.
55#[derive(Debug, Clone)]
56pub struct DnatRule {
57    pub orig_addr: Ipv4Addr,
58    pub orig_port: Option<u16>,
59    pub new_addr: Ipv4Addr,
60    pub new_port: Option<u16>,
61}
62
63/// Configuration for [`ChainEngine`].
64#[derive(Debug, Clone)]
65pub struct ChainConfig {
66    pub proxies: Vec<ProxyEntry>,
67    pub chain_type: ChainType,
68    /// Number of proxies to use per connection (for Random / RoundRobin).
69    pub chain_len: usize,
70    pub connect_timeout: Duration,
71    pub localnets: Vec<LocalNet>,
72    pub dnats: Vec<DnatRule>,
73}
74
75impl Default for ChainConfig {
76    fn default() -> Self {
77        ChainConfig {
78            proxies: Vec::new(),
79            chain_type: ChainType::Dynamic,
80            chain_len: 1,
81            connect_timeout: Duration::from_secs(10),
82            localnets: Vec::new(),
83            dnats: Vec::new(),
84        }
85    }
86}
87
88// ─── ChainEngine ─────────────────────────────────────────────────────────────
89
90/// Routes outbound TCP connections through a configurable chain of proxies.
91pub struct ChainEngine {
92    config: ChainConfig,
93    /// Shared offset for round-robin mode.
94    rr_offset: AtomicUsize,
95}
96
97impl ChainEngine {
98    /// Create a new engine from `config`.
99    pub fn new(config: ChainConfig) -> Self {
100        ChainEngine {
101            config,
102            rr_offset: AtomicUsize::new(0),
103        }
104    }
105
106    /// Open a connection to `target` through the proxy chain.
107    ///
108    /// If the target matches a `localnet` rule, a direct connection is made.
109    /// DNAT rewrites are applied before localnet and proxy selection.
110    ///
111    /// Returns a [`BoxStream`] so that TLS-wrapped hops (HTTPS proxies) and
112    /// plain TCP hops share the same return type.
113    pub async fn connect(&self, target: Target) -> Result<BoxStream> {
114        let target = self.apply_dnat(target);
115
116        if self.is_localnet(&target) {
117            return self.direct_connect(&target).await;
118        }
119
120        match self.config.chain_type {
121            ChainType::Strict => self.connect_strict(target).await,
122            ChainType::Dynamic => self.connect_dynamic(target).await,
123            ChainType::Random => self.connect_random(target).await,
124            ChainType::RoundRobin => self.connect_round_robin(target).await,
125        }
126    }
127
128    // ── Chain modes ───────────────────────────────────────────────────────────
129    //
130    // All four modes use the same anchor-advance strategy via `chain_proxies`:
131    // each proxy in the (mode-specific) selection is tried as the chain entry
132    // point in order; if the TCP connect or any subsequent handshake fails, the
133    // anchor advances to the next candidate.  This skips broken proxies in all
134    // modes without risking infinite loops.
135
136    async fn connect_strict(&self, target: Target) -> Result<BoxStream> {
137        let refs: Vec<&ProxyEntry> = self.config.proxies.iter().collect();
138        if refs.is_empty() {
139            bail!("strict_chain: no proxies configured");
140        }
141        self.chain_proxies(&refs, target, "strict")
142            .await
143            .context("strict_chain")
144    }
145
146    async fn connect_dynamic(&self, target: Target) -> Result<BoxStream> {
147        let refs: Vec<&ProxyEntry> = self.config.proxies.iter().collect();
148        if refs.is_empty() {
149            bail!("dynamic_chain: no proxies configured");
150        }
151        self.chain_proxies(&refs, target, "dynamic")
152            .await
153            .context("dynamic_chain")
154    }
155
156    async fn connect_random(&self, target: Target) -> Result<BoxStream> {
157        let chain_len = self.config.chain_len.max(1);
158        let proxies = &self.config.proxies;
159        if proxies.len() < chain_len {
160            bail!(
161                "random_chain: need {chain_len} proxies, only {} available",
162                proxies.len()
163            );
164        }
165        // OsRng reads directly from the OS entropy source (getrandom) on every
166        // call, guaranteeing different selections across runs and across
167        // connections within a run.  It is also Send, so the future remains
168        // Send without any drop-before-await gymnastics.
169        let mut selected: Vec<&ProxyEntry> = proxies.iter().collect();
170        selected.shuffle(&mut OsRng);
171        selected.truncate(chain_len);
172        self.chain_proxies(&selected, target, "random")
173            .await
174            .context("random_chain")
175    }
176
177    async fn connect_round_robin(&self, target: Target) -> Result<BoxStream> {
178        let chain_len = self.config.chain_len.max(1);
179        let proxies = &self.config.proxies;
180        if proxies.is_empty() {
181            bail!("round_robin_chain: no proxies");
182        }
183        let n = proxies.len();
184        let offset = self.rr_offset.fetch_add(chain_len, Ordering::SeqCst) % n;
185        let selected: Vec<&ProxyEntry> =
186            (0..chain_len).map(|i| &proxies[(offset + i) % n]).collect();
187        self.chain_proxies(&selected, target, "round-robin")
188            .await
189            .context("round_robin_chain")
190    }
191
192    // ── Helpers ───────────────────────────────────────────────────────────────
193
194    /// Build a proxy chain from a pre-selected proxy slice, skipping broken hops.
195    ///
196    /// Tries each proxy in `proxies` as the chain anchor in order.  If the TCP
197    /// connect succeeds but a downstream handshake fails, the anchor advances so
198    /// the same dead proxy is never retried as an entry point (guaranteeing
199    /// termination in O(n) attempts).  Succeeds as long as at least one proxy
200    /// in the slice can reach the target.
201    ///
202    /// On success logs the chain path at `info` level:
203    /// `|<label>-chain| proxy1:port → proxy2:port → target OK`
204    async fn chain_proxies(
205        &self,
206        proxies: &[&ProxyEntry],
207        target: Target,
208        label: &str,
209    ) -> Result<BoxStream> {
210        if proxies.is_empty() {
211            bail!("chain_proxies: empty proxy list");
212        }
213        for (anchor, proxy) in proxies.iter().enumerate() {
214            let stream = match self.tcp_connect(proxy.addr, proxy.port).await {
215                Ok(s) => s,
216                Err(_) => continue, // anchor unreachable; try the next one
217            };
218            match chain_from(stream, anchor, proxies, target.clone()).await {
219                Ok(s) => {
220                    // Log the successful chain path so the user can see which
221                    // proxies were actually used (matching proxychains-ng style).
222                    let hops = proxies[anchor..]
223                        .iter()
224                        .map(|p| format!("{}:{}", p.addr, p.port))
225                        .collect::<Vec<_>>()
226                        .join(" → ");
227                    info!("|{label}-chain| {hops} → {target}");
228                    return Ok(s);
229                }
230                Err(_) => continue, // dead intermediate hop; advance anchor
231            }
232        }
233        bail!("chain_proxies: all proxies are unreachable")
234    }
235
236    async fn tcp_connect(&self, addr: IpAddr, port: u16) -> Result<BoxStream> {
237        let stream = timeout(
238            self.config.connect_timeout,
239            TcpStream::connect((addr, port)),
240        )
241        .await
242        .context("tcp connect timed out")?
243        .context("tcp connect failed")?;
244        Ok(Box::new(stream))
245    }
246
247    async fn direct_connect(&self, target: &Target) -> Result<BoxStream> {
248        let stream = match target {
249            Target::Ip(ip, port) => timeout(
250                self.config.connect_timeout,
251                TcpStream::connect((*ip, *port)),
252            )
253            .await
254            .context("direct connect timed out")?
255            .context("direct connect failed")?,
256            Target::Host(h, p) => timeout(
257                self.config.connect_timeout,
258                TcpStream::connect(format!("{h}:{p}").as_str()),
259            )
260            .await
261            .context("direct connect timed out")?
262            .context("direct connect failed")?,
263        };
264        Ok(Box::new(stream))
265    }
266
267    fn apply_dnat(&self, target: Target) -> Target {
268        if let Target::Ip(IpAddr::V4(ip), port) = &target {
269            for rule in &self.config.dnats {
270                if rule.orig_addr == *ip {
271                    if let Some(orig_port) = rule.orig_port {
272                        if orig_port != *port {
273                            continue;
274                        }
275                    }
276                    let new_port = rule.new_port.unwrap_or(*port);
277                    return Target::Ip(IpAddr::V4(rule.new_addr), new_port);
278                }
279            }
280        }
281        target
282    }
283
284    fn is_localnet(&self, target: &Target) -> bool {
285        let (ip, port) = match target {
286            Target::Ip(ip, p) => (Some(*ip), *p),
287            Target::Host(_, p) => (None, *p),
288        };
289        let Some(ip) = ip else { return false };
290
291        for ln in &self.config.localnets {
292            if let Some(p) = ln.port {
293                if p != port {
294                    continue;
295                }
296            }
297            match (ip, ln.addr) {
298                (IpAddr::V4(tip), IpAddr::V4(laddr)) => {
299                    if let Some(mask) = ln.mask_v4 {
300                        let t = u32::from(tip);
301                        let l = u32::from(laddr);
302                        let m = u32::from(mask);
303                        if (t & m) == (l & m) {
304                            return true;
305                        }
306                    }
307                }
308                (IpAddr::V6(tip), IpAddr::V6(laddr)) => {
309                    if let Some(prefix) = ln.prefix_v6 {
310                        if ipv6_match(tip, laddr, prefix) {
311                            return true;
312                        }
313                    }
314                }
315                _ => {}
316            }
317        }
318        false
319    }
320}
321
322fn ipv6_match(a: Ipv6Addr, b: Ipv6Addr, prefix: u8) -> bool {
323    let a = a.octets();
324    let b = b.octets();
325    let full = (prefix / 8) as usize;
326    let rem = prefix % 8;
327    if a[..full] != b[..full] {
328        return false;
329    }
330    if rem > 0 {
331        let mask = 0xFFu8 << (8 - rem);
332        if (a[full] & mask) != (b[full] & mask) {
333            return false;
334        }
335    }
336    true
337}
338
339/// Build a [`Target`] pointing to `proxy`'s address (used as intermediate hop).
340fn hop_target(proxy: &ProxyEntry) -> Target {
341    Target::Ip(proxy.addr, proxy.port)
342}
343
344/// Perform the appropriate protocol handshake for `prev_proxy` to connect to `next_target`.
345async fn handshake(
346    stream: BoxStream,
347    prev_proxy: &ProxyEntry,
348    next_target: Target,
349) -> Result<BoxStream> {
350    let user = prev_proxy.username.as_deref();
351    let pass = prev_proxy.password.as_deref();
352    match prev_proxy.proxy_type {
353        ProxyType::Socks4 => socks4::connect(stream, &next_target, user).await,
354        ProxyType::Socks5 => socks5::connect(stream, &next_target, user, pass).await,
355        ProxyType::Http => http::connect(stream, &next_target, user, pass).await,
356        ProxyType::Https => https::connect(stream, &next_target, user, pass, prev_proxy.addr).await,
357        ProxyType::Raw => raw::connect(stream, &next_target).await,
358    }
359}
360
361/// Drive the proxy chain from `proxies[anchor]` outward to `target`.
362///
363/// The caller has already opened a TCP connection to `proxies[anchor]`.  This
364/// function issues CONNECT handshakes for each subsequent hop and finally for
365/// `target`, returning the fully-tunneled stream on success.
366///
367/// Returns `Err` on the first failed handshake so that `chain_proxies` can
368/// advance the anchor and retry from the next proxy.
369async fn chain_from(
370    mut stream: BoxStream,
371    anchor: usize,
372    proxies: &[&ProxyEntry],
373    target: Target,
374) -> Result<BoxStream> {
375    // Issue CONNECT for each intermediate hop: anchor → anchor+1 → … → last.
376    for i in anchor..proxies.len() - 1 {
377        stream = handshake(stream, proxies[i], hop_target(proxies[i + 1])).await?;
378    }
379    // Final handshake: last proxy in the slice → target.
380    handshake(stream, proxies[proxies.len() - 1], target).await
381}