feat: add probe json/stats and strengthen ci-release automation
This commit is contained in:
1 parent
e70e6c3b4c
commit
bd0d6205ac
8 files changed
+413
-28
No files matched your search
@@ -43,3 +43,70 @@ jobs:
|
||||
|
||||
- name: Build release binaries
|
||||
run: cargo build --release --workspace
|
||||
|
||||
linux-e2e-distros:
|
||||
name: linux-e2e (${{ matrix.distro }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- distro: ubuntu-24.04
|
||||
family: deb
|
||||
image: ubuntu:24.04
|
||||
- distro: debian-12
|
||||
family: deb
|
||||
image: debian:12
|
||||
- distro: fedora-41
|
||||
family: rpm
|
||||
image: fedora:41
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run e2e in distro container
|
||||
shell: bash
|
||||
env:
|
||||
DISTRO_IMAGE: ${{ matrix.image }}
|
||||
DISTRO_FAMILY: ${{ matrix.family }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -e DISTRO_FAMILY="${DISTRO_FAMILY}" -v "$PWD:/work" -w /work "${DISTRO_IMAGE}" /bin/bash -lc '
|
||||
set -euo pipefail
|
||||
if [ "${DISTRO_FAMILY}" = "deb" ]; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
curl ca-certificates git build-essential pkg-config python3 python3-pip
|
||||
else
|
||||
dnf install -y \
|
||||
curl ca-certificates git gcc gcc-c++ make pkgconf-pkg-config python3 python3-pip
|
||||
fi
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain stable
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
rustup --version
|
||||
cargo --version
|
||||
|
||||
pip3 install --no-input proxy.py pproxy
|
||||
nohup python3 -m pproxy -l socks5://127.0.0.1:19080 >/tmp/pproxy-socks5.log 2>&1 &
|
||||
nohup python3 -m proxy --hostname 127.0.0.1 --port 18080 >/tmp/proxy-http.log 2>&1 &
|
||||
sleep 2
|
||||
|
||||
printf "%s\n" \
|
||||
"dynamic_chain" \
|
||||
"proxy_dns" \
|
||||
"tcp_connect_time_out 3000" \
|
||||
"tcp_read_time_out 8000" \
|
||||
"[ProxyList]" \
|
||||
"socks5 127.0.0.1 19080" \
|
||||
"http 127.0.0.1 18080" > ci.e2e.conf
|
||||
|
||||
cargo run -p proxychains-bin -- -f ./ci.e2e.conf --probe
|
||||
cargo run -p proxychains-bin -- -f ./ci.e2e.conf --probe --probe-json > probe-report.json
|
||||
grep -q "\"summary\"" probe-report.json
|
||||
|
||||
PROXYCHAINS_TEST_SOCKS5_ADDR=127.0.0.1:19080 \
|
||||
PROXYCHAINS_TEST_HTTP_ADDR=127.0.0.1:18080 \
|
||||
cargo test -p proxychains --test real_backends -- --nocapture
|
||||
'
|
||||
@@ -96,15 +96,110 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Checkout (for changelog generation)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-assets
|
||||
|
||||
- name: Generate checksums
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
find release-assets -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' \) -print0 \
|
||||
| sort -z \
|
||||
| xargs -0 sha256sum > SHA256SUMS.txt
|
||||
|
||||
- name: Generate release notes and changelog
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
PREV_TAG="$(git tag --sort=-creatordate | grep -v "^${TAG}$" | head -n1 || true)"
|
||||
if [ -n "${PREV_TAG}" ]; then
|
||||
RANGE="${PREV_TAG}..${TAG}"
|
||||
else
|
||||
RANGE="${TAG}"
|
||||
fi
|
||||
|
||||
mapfile -t COMMITS < <(git log --no-merges --pretty=format:'%s' ${RANGE})
|
||||
|
||||
section() {
|
||||
local name="$1"
|
||||
shift
|
||||
echo "### ${name}" >> CHANGELOG_AUTO.md
|
||||
local found=0
|
||||
for c in "${COMMITS[@]}"; do
|
||||
for prefix in "$@"; do
|
||||
if [[ "${c}" == ${prefix}* ]]; then
|
||||
echo "- ${c}" >> CHANGELOG_AUTO.md
|
||||
found=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [ "${found}" -eq 0 ]; then
|
||||
echo "- (none)" >> CHANGELOG_AUTO.md
|
||||
fi
|
||||
echo "" >> CHANGELOG_AUTO.md
|
||||
}
|
||||
|
||||
{
|
||||
echo "## Changelog (${TAG})"
|
||||
echo ""
|
||||
} > CHANGELOG_AUTO.md
|
||||
section "Features" "feat:" "feature:"
|
||||
section "Fixes" "fix:"
|
||||
section "Docs" "docs:"
|
||||
section "CI/Build" "ci:" "build:" "chore:"
|
||||
echo "### Other" >> CHANGELOG_AUTO.md
|
||||
OTHER_FOUND=0
|
||||
for c in "${COMMITS[@]}"; do
|
||||
if [[ "${c}" != feat:* && "${c}" != feature:* && "${c}" != fix:* && "${c}" != docs:* && "${c}" != ci:* && "${c}" != build:* && "${c}" != chore:* ]]; then
|
||||
echo "- ${c}" >> CHANGELOG_AUTO.md
|
||||
OTHER_FOUND=1
|
||||
fi
|
||||
done
|
||||
if [ "${OTHER_FOUND}" -eq 0 ]; then
|
||||
echo "- (none)" >> CHANGELOG_AUTO.md
|
||||
fi
|
||||
echo "" >> CHANGELOG_AUTO.md
|
||||
|
||||
cat > RELEASE_NOTES.md <<EOF
|
||||
## proxychains-rs ${TAG}
|
||||
|
||||
### Minimal Usage
|
||||
|
||||
\`\`\`bash
|
||||
# Linux/macOS
|
||||
./proxychains4 -f ./proxychains.conf --probe
|
||||
./proxychains4 -f ./proxychains.conf curl https://ifconfig.me
|
||||
|
||||
# Windows
|
||||
.\\proxychains4.exe -f .\\proxychains.conf --probe
|
||||
.\\proxychains4.exe -f .\\proxychains.conf curl https://ifconfig.me
|
||||
\`\`\`
|
||||
|
||||
### Assets
|
||||
- Windows zip (\`proxychains4.exe\`, \`proxychains.dll\`)
|
||||
- Linux tar.gz
|
||||
- Linux deb
|
||||
- macOS tar.gz
|
||||
- SHA256 checksums: \`SHA256SUMS.txt\`
|
||||
|
||||
EOF
|
||||
cat CHANGELOG_AUTO.md >> RELEASE_NOTES.md
|
||||
|
||||
- name: Publish release assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: RELEASE_NOTES.md
|
||||
files: |
|
||||
release-assets/**/*.tar.gz
|
||||
release-assets/**/*.zip
|
||||
release-assets/**/*.deb
|
||||
SHA256SUMS.txt
|
||||
@@ -31,3 +31,5 @@ socket2 = { version = "0.5", features = ["all"] }
|
||||
rand = "0.8"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
@@ -41,6 +41,7 @@ Recommendation:
|
||||
- `--list-groups`
|
||||
- `--check`
|
||||
- `--probe` (quick per-node reachability/latency check)
|
||||
- `--probe-json` (machine-readable probe output)
|
||||
- DNS:
|
||||
- `proxy_dns`
|
||||
- compatibility aliases: `proxy_dns_old`, `proxy_dns_daemon`
|
||||
@@ -91,6 +92,7 @@ proxychains4 -f ./proxychains.conf --list-groups
|
||||
proxychains4 -f ./proxychains.conf --group jp --check
|
||||
proxychains4 -f ./proxychains.conf --group jp --probe
|
||||
proxychains4 -f ./proxychains.conf --probe --probe-timeout-ms 1200
|
||||
proxychains4 -f ./proxychains.conf --probe --probe-json
|
||||
```
|
||||
|
||||
## Example Config
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
- `--list-groups`
|
||||
- `--check`
|
||||
- `--probe`(快速探测每个节点连通性与时延)
|
||||
- `--probe-json`(机器可读 JSON 输出)
|
||||
- DNS:
|
||||
- `proxy_dns`
|
||||
- 兼容别名:`proxy_dns_old`、`proxy_dns_daemon`
|
||||
@@ -91,6 +92,7 @@ proxychains4 -f ./proxychains.conf --list-groups
|
||||
proxychains4 -f ./proxychains.conf --group jp --check
|
||||
proxychains4 -f ./proxychains.conf --group jp --probe
|
||||
proxychains4 -f ./proxychains.conf --probe --probe-timeout-ms 1200
|
||||
proxychains4 -f ./proxychains.conf --probe --probe-json
|
||||
```
|
||||
|
||||
## 示例配置
|
||||
|
||||
@@ -18,6 +18,8 @@ clap.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
+158
-28
@@ -7,12 +7,14 @@
|
||||
//! - Windows: Uses DLL injection
|
||||
|
||||
use std::env;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::{SocketAddr, SocketAddrV4, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use clap::Parser;
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, error, info, Level};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
@@ -57,6 +59,10 @@ struct Args {
|
||||
#[arg(long, value_name = "MS")]
|
||||
probe_timeout_ms: Option<u64>,
|
||||
|
||||
/// Print probe result as JSON (machine-readable)
|
||||
#[arg(long)]
|
||||
probe_json: bool,
|
||||
|
||||
/// The command to run
|
||||
#[arg(
|
||||
required_unless_present_any = ["list_groups", "check", "probe"],
|
||||
@@ -211,13 +217,8 @@ fn run_probe(config: &Config, args: &Args) -> usize {
|
||||
.map(Duration::from_millis)
|
||||
.unwrap_or(config.tcp_connect_timeout);
|
||||
let mut failed = 0usize;
|
||||
|
||||
println!("Proxy probe:");
|
||||
println!(" timeout_ms={}", timeout.as_millis());
|
||||
println!(
|
||||
" group={}",
|
||||
args.group.as_deref().unwrap_or("default/all")
|
||||
);
|
||||
let mut results = Vec::with_capacity(config.proxies.len());
|
||||
let selected_group = args.group.as_deref().unwrap_or("default/all").to_string();
|
||||
|
||||
for (idx, proxy) in config.proxies.iter().enumerate() {
|
||||
let target_v4 = SocketAddrV4::new(proxy.ip, proxy.port);
|
||||
@@ -227,38 +228,159 @@ fn run_probe(config: &Config, args: &Args) -> usize {
|
||||
Ok(stream) => {
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
let _ = stream.shutdown(std::net::Shutdown::Both);
|
||||
println!(
|
||||
" [{}] OK {:<6} {}:{} {} ms",
|
||||
idx + 1,
|
||||
proxy.proxy_type,
|
||||
proxy.ip,
|
||||
proxy.port,
|
||||
elapsed
|
||||
);
|
||||
results.push(ProbeNode {
|
||||
index: idx + 1,
|
||||
proxy_type: proxy.proxy_type.to_string(),
|
||||
address: format!("{}:{}", proxy.ip, proxy.port),
|
||||
ok: true,
|
||||
latency_ms: elapsed,
|
||||
failure_type: None,
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
failed += 1;
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
println!(
|
||||
" [{}] FAIL {:<6} {}:{} {} ms ({})",
|
||||
idx + 1,
|
||||
proxy.proxy_type,
|
||||
proxy.ip,
|
||||
proxy.port,
|
||||
elapsed,
|
||||
e
|
||||
);
|
||||
let failure_type = classify_probe_error(&e).to_string();
|
||||
results.push(ProbeNode {
|
||||
index: idx + 1,
|
||||
proxy_type: proxy.proxy_type.to_string(),
|
||||
address: format!("{}:{}", proxy.ip, proxy.port),
|
||||
ok: false,
|
||||
latency_ms: elapsed,
|
||||
failure_type: Some(failure_type),
|
||||
error: Some(e.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let report = build_probe_report(results, timeout, selected_group);
|
||||
if args.probe_json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
|
||||
);
|
||||
} else {
|
||||
print_probe_report(&report);
|
||||
}
|
||||
failed
|
||||
}
|
||||
|
||||
fn classify_probe_error(err: &std::io::Error) -> &'static str {
|
||||
match err.kind() {
|
||||
ErrorKind::TimedOut => "timeout",
|
||||
ErrorKind::ConnectionRefused => "refused",
|
||||
ErrorKind::ConnectionReset => "reset",
|
||||
ErrorKind::NetworkUnreachable => "network_unreachable",
|
||||
ErrorKind::AddrNotAvailable => "addr_unavailable",
|
||||
ErrorKind::NotConnected => "not_connected",
|
||||
_ => "other",
|
||||
}
|
||||
}
|
||||
|
||||
fn build_probe_report(results: Vec<ProbeNode>, timeout: Duration, group: String) -> ProbeReport {
|
||||
let total = results.len();
|
||||
let ok = results.iter().filter(|r| r.ok).count();
|
||||
let fail = total.saturating_sub(ok);
|
||||
let mut stats = ProbeFailureStats::default();
|
||||
for r in &results {
|
||||
if r.ok {
|
||||
continue;
|
||||
}
|
||||
match r.failure_type.as_deref() {
|
||||
Some("timeout") => stats.timeout += 1,
|
||||
Some("refused") => stats.refused += 1,
|
||||
Some("reset") => stats.reset += 1,
|
||||
Some("network_unreachable") => stats.network_unreachable += 1,
|
||||
Some("addr_unavailable") => stats.addr_unavailable += 1,
|
||||
Some("not_connected") => stats.not_connected += 1,
|
||||
_ => stats.other += 1,
|
||||
}
|
||||
}
|
||||
ProbeReport {
|
||||
timeout_ms: timeout.as_millis(),
|
||||
selected_group: group,
|
||||
summary: ProbeSummary { total, ok, fail },
|
||||
failure_stats: stats,
|
||||
nodes: results,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_probe_report(report: &ProbeReport) {
|
||||
println!("Proxy probe:");
|
||||
println!(" timeout_ms={}", report.timeout_ms);
|
||||
println!(" group={}", report.selected_group);
|
||||
for n in &report.nodes {
|
||||
if n.ok {
|
||||
println!(
|
||||
" [{}] OK {:<8} {} {} ms",
|
||||
n.index, n.proxy_type, n.address, n.latency_ms
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" [{}] FAIL {:<8} {} {} ms [{}] ({})",
|
||||
n.index,
|
||||
n.proxy_type,
|
||||
n.address,
|
||||
n.latency_ms,
|
||||
n.failure_type.as_deref().unwrap_or("other"),
|
||||
n.error.as_deref().unwrap_or("unknown")
|
||||
);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"Probe summary: total={}, ok={}, fail={}",
|
||||
config.proxies.len(),
|
||||
config.proxies.len().saturating_sub(failed),
|
||||
failed
|
||||
report.summary.total, report.summary.ok, report.summary.fail
|
||||
);
|
||||
failed
|
||||
println!(
|
||||
"Failure stats: timeout={}, refused={}, reset={}, net_unreach={}, addr_unavail={}, not_connected={}, other={}",
|
||||
report.failure_stats.timeout,
|
||||
report.failure_stats.refused,
|
||||
report.failure_stats.reset,
|
||||
report.failure_stats.network_unreachable,
|
||||
report.failure_stats.addr_unavailable,
|
||||
report.failure_stats.not_connected,
|
||||
report.failure_stats.other
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ProbeReport {
|
||||
timeout_ms: u128,
|
||||
selected_group: String,
|
||||
summary: ProbeSummary,
|
||||
failure_stats: ProbeFailureStats,
|
||||
nodes: Vec<ProbeNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ProbeSummary {
|
||||
total: usize,
|
||||
ok: usize,
|
||||
fail: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Default)]
|
||||
struct ProbeFailureStats {
|
||||
timeout: usize,
|
||||
refused: usize,
|
||||
reset: usize,
|
||||
network_unreachable: usize,
|
||||
addr_unavailable: usize,
|
||||
not_connected: usize,
|
||||
other: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ProbeNode {
|
||||
index: usize,
|
||||
proxy_type: String,
|
||||
address: String,
|
||||
ok: bool,
|
||||
latency_ms: u128,
|
||||
failure_type: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
/// Set proxychains-specific environment variables
|
||||
@@ -505,4 +627,12 @@ mod tests {
|
||||
assert!(args.probe);
|
||||
assert!(args.command.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_args_probe_json() {
|
||||
let args = Args::try_parse_from(["proxychains4", "--probe", "--probe-json"]);
|
||||
assert!(args.is_ok());
|
||||
let args = args.unwrap();
|
||||
assert!(args.probe_json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
use proxychains::config::{ProxyData, ProxyType};
|
||||
use proxychains::proxy::{
|
||||
http_connect, socks5_connect, establish_proxy_chain, TargetAddr, TargetAddress,
|
||||
};
|
||||
|
||||
fn proxy_from_env(var: &str, proxy_type: ProxyType) -> Option<ProxyData> {
|
||||
let raw = std::env::var(var).ok()?;
|
||||
let addr: SocketAddr = raw.parse().ok()?;
|
||||
let ip = match addr.ip() {
|
||||
IpAddr::V4(v4) => v4,
|
||||
IpAddr::V6(_) => return None,
|
||||
};
|
||||
Some(ProxyData::new(ip, addr.port(), proxy_type))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn socks5_backend_handshake_works() {
|
||||
let Some(proxy) = proxy_from_env("PROXYCHAINS_TEST_SOCKS5_ADDR", ProxyType::Socks5) else {
|
||||
eprintln!("skip: PROXYCHAINS_TEST_SOCKS5_ADDR not set");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut stream = TcpStream::connect_timeout(
|
||||
&SocketAddr::new(IpAddr::V4(proxy.ip), proxy.port),
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.expect("connect socks5 proxy");
|
||||
|
||||
let target = TargetAddr::from_domain("example.com");
|
||||
socks5_connect(&mut stream, &proxy, &target, 80, Duration::from_secs(8))
|
||||
.expect("socks5 connect to target");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_connect_backend_handshake_works() {
|
||||
let Some(proxy) = proxy_from_env("PROXYCHAINS_TEST_HTTP_ADDR", ProxyType::Http) else {
|
||||
eprintln!("skip: PROXYCHAINS_TEST_HTTP_ADDR not set");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut stream = TcpStream::connect_timeout(
|
||||
&SocketAddr::new(IpAddr::V4(proxy.ip), proxy.port),
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.expect("connect http proxy");
|
||||
|
||||
http_connect(
|
||||
&mut stream,
|
||||
&proxy,
|
||||
"example.com",
|
||||
443,
|
||||
Duration::from_secs(8),
|
||||
)
|
||||
.expect("http CONNECT to target");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn establish_chain_with_real_socks5_works() {
|
||||
let Some(proxy) = proxy_from_env("PROXYCHAINS_TEST_SOCKS5_ADDR", ProxyType::Socks5) else {
|
||||
eprintln!("skip: PROXYCHAINS_TEST_SOCKS5_ADDR not set");
|
||||
return;
|
||||
};
|
||||
|
||||
let target = TargetAddress::from_domain("example.com");
|
||||
let stream = establish_proxy_chain(
|
||||
&[proxy],
|
||||
&target,
|
||||
80,
|
||||
Duration::from_secs(3),
|
||||
Duration::from_secs(8),
|
||||
)
|
||||
.expect("establish chain to target via real socks5");
|
||||
|
||||
let _ = stream.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_group_probe_demo_shape() {
|
||||
// Sanity regression around types expected by CLI probe json output.
|
||||
let proxy = ProxyData::new(Ipv4Addr::new(127, 0, 0, 1), 1080, ProxyType::Socks5);
|
||||
assert_eq!(proxy.proxy_type.to_string(), "socks5");
|
||||
}
|
||||
Reference in new issue
Block a user