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#[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 Https,
32 Raw,
33}
34
35#[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#[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#[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#[derive(Debug, Clone)]
65pub struct ChainConfig {
66 pub proxies: Vec<ProxyEntry>,
67 pub chain_type: ChainType,
68 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
88pub struct ChainEngine {
92 config: ChainConfig,
93 rr_offset: AtomicUsize,
95}
96
97impl ChainEngine {
98 pub fn new(config: ChainConfig) -> Self {
100 ChainEngine {
101 config,
102 rr_offset: AtomicUsize::new(0),
103 }
104 }
105
106 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 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 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 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, };
218 match chain_from(stream, anchor, proxies, target.clone()).await {
219 Ok(s) => {
220 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, }
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
339fn hop_target(proxy: &ProxyEntry) -> Target {
341 Target::Ip(proxy.addr, proxy.port)
342}
343
344async 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
361async fn chain_from(
370 mut stream: BoxStream,
371 anchor: usize,
372 proxies: &[&ProxyEntry],
373 target: Target,
374) -> Result<BoxStream> {
375 for i in anchor..proxies.len() - 1 {
377 stream = handshake(stream, proxies[i], hop_target(proxies[i + 1])).await?;
378 }
379 handshake(stream, proxies[proxies.len() - 1], target).await
381}