feat(client): implement alknet-client crate — AlknetClient, dial methods, error type, SOCKS5 proxy
- Initialize alknet-client crate with Cargo.toml, deps, feature flags - Implement ClientDialError enum with 5 variants (TlsConfig, Connect, Handshake, NoTransport, Proxy) - Implement AlknetClient struct with builder methods (with_quinn, with_tcp_tls, with_iroh, with_socks5_proxy) - Implement dial_quic — QUIC dial via quinn, producing a Connection - Implement dial_tcp_tls — TCP+TLS dial via tokio-rustls, producing a Connection - Implement dial_iroh — iroh dial, producing a Connection - Implement SOCKS5 proxy support — Socks5ProxyConfig, Socks5Credentials, Socks5UdpSocket, proxy integration - Add into_rustls_config() to TlsClientConfig in alknet-tls - Add alknet-client to workspace members Tasks: client/crate-init, client/error-type, client/client-core, client/dial-quic, client/dial-tcp-tls, client/dial-iroh, client/socks5-proxy
This commit is contained in:
1 parent
b476182d64
commit
82ee37e206
12 files changed
+1013
-8
No files matched your search
Generated
+51
-8
@@ -68,6 +68,24 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alknet-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"alknet-core",
|
||||||
|
"alknet-tls",
|
||||||
|
"fast-socks5",
|
||||||
|
"hex",
|
||||||
|
"iroh",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alknet-core"
|
name = "alknet-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1296,6 +1314,21 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fast-socks5"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9545787d8304a71e1bf1b711705070a4c400cce9b332c4a11800627b7c9a2067"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"log",
|
||||||
|
"socket2 0.5.10",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastbloom"
|
name = "fastbloom"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
@@ -1834,7 +1867,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -2006,7 +2039,7 @@ version = "0.3.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"widestring",
|
"widestring",
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
"windows-result",
|
"windows-result",
|
||||||
@@ -2595,7 +2628,7 @@ dependencies = [
|
|||||||
"objc2-system-configuration",
|
"objc2-system-configuration",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"serde",
|
"serde",
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -2642,7 +2675,7 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -2685,7 +2718,7 @@ checksum = "3137a52df66c20090a889828d1c655f21f52294cba64e5c4fbb04fc83eee7c8e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg_aliases 0.2.1",
|
"cfg_aliases 0.2.1",
|
||||||
"libc",
|
"libc",
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -3213,7 +3246,7 @@ dependencies = [
|
|||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -3253,7 +3286,7 @@ dependencies = [
|
|||||||
"cfg_aliases 0.2.1",
|
"cfg_aliases 0.2.1",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
@@ -4093,6 +4126,16 @@ version = "1.15.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.5.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -4388,7 +4431,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2 0.6.4",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"crates/alknet-tls",
|
"crates/alknet-tls",
|
||||||
"crates/alknet-tty",
|
"crates/alknet-tty",
|
||||||
"crates/alknet-tty-local",
|
"crates/alknet-tty-local",
|
||||||
|
"crates/alknet-client",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "alknet-client"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Native client dial seam — multi-transport dialer that produces Connections for protocol take-overs"
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "alknet_client"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
quinn = ["dep:quinn", "alknet-tls/quinn", "alknet-core/quinn"]
|
||||||
|
tcp = ["dep:tokio-rustls", "alknet-tls/tcp"]
|
||||||
|
iroh = ["dep:iroh", "alknet-core/iroh"]
|
||||||
|
socks5 = ["dep:fast-socks5"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alknet-core = { path = "../alknet-core" }
|
||||||
|
alknet-tls = { path = "../alknet-tls" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
thiserror = "2"
|
||||||
|
tracing = "0.1"
|
||||||
|
quinn = { version = "0.11", optional = true }
|
||||||
|
tokio-rustls = { version = "0.26", optional = true }
|
||||||
|
iroh = { version = "1.0", optional = true, default-features = false, features = ["tls-aws-lc-rs"] }
|
||||||
|
fast-socks5 = { version = "1", optional = true }
|
||||||
|
rustls = "0.23"
|
||||||
|
rustls-pki-types = "1"
|
||||||
|
hex = "0.4"
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
//! `AlknetClient` — native client dial seam, the client-side analogue of
|
||||||
|
//! `AlknetEndpoint`. Holds pre-built transport handles, all optional — the
|
||||||
|
//! client dials with whichever transport the remote endpoint type implies.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
use quinn;
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
use tokio_rustls;
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
use iroh;
|
||||||
|
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
use crate::socks5::Socks5ProxyConfig;
|
||||||
|
|
||||||
|
/// Native client dial seam — multi-transport dialer that produces
|
||||||
|
/// `Connection`s for protocol take-overs.
|
||||||
|
///
|
||||||
|
/// Holds pre-built transport handles, all optional — the client dials
|
||||||
|
/// with whichever transport the remote endpoint type implies. The
|
||||||
|
/// builder mirrors `AlknetEndpoint`'s `with_quinn` / `with_iroh` /
|
||||||
|
/// `with_tcp_tls` (ADR-083) — the assembly layer builds the transport
|
||||||
|
/// handles and hands them to the client via builder methods.
|
||||||
|
pub struct AlknetClient {
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub(crate) quinn: Option<quinn::Endpoint>,
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
pub(crate) tcp_connector: Option<tokio_rustls::TlsConnector>,
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub(crate) iroh: Option<iroh::Endpoint>,
|
||||||
|
/// When set, `dial_quic` and `dial_tcp_tls` route through this
|
||||||
|
/// SOCKS5 proxy (UDP ASSOCIATE / CONNECT respectively). `dial_iroh`
|
||||||
|
/// forces relay-only via an HTTP-to-SOCKS5 bridge — see ADR-090 §5.
|
||||||
|
/// Feature-gated on `socks5`.
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
pub(crate) socks5: Option<Socks5ProxyConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlknetClient {
|
||||||
|
/// Create a new `AlknetClient` with no transport handles configured.
|
||||||
|
/// Use the builder methods to add transports.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
quinn: None,
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
tcp_connector: None,
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
iroh: None,
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
socks5: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the QUIC transport handle. The assembly layer builds a
|
||||||
|
/// `quinn::Endpoint` (with or without a SOCKS5 proxy — the proxy
|
||||||
|
/// is applied inside `dial_quic`, not at construction time) and
|
||||||
|
/// hands it to the client.
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self {
|
||||||
|
self.quinn = Some(endpoint);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the TCP+TLS transport handle. The assembly layer builds a
|
||||||
|
/// `tokio_rustls::TlsConnector` and hands it to the client.
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
pub fn with_tcp_tls(mut self, connector: tokio_rustls::TlsConnector) -> Self {
|
||||||
|
self.tcp_connector = Some(connector);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the iroh transport handle. The assembly layer builds an
|
||||||
|
/// `iroh::Endpoint` and hands it to the client.
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self {
|
||||||
|
self.iroh = Some(endpoint);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the SOCKS5 proxy for all subsequent dials. When set, every
|
||||||
|
/// dial routes its transport through this proxy: UDP ASSOCIATE for
|
||||||
|
/// `dial_quic`, CONNECT for `dial_tcp_tls`, and force-relay-only +
|
||||||
|
/// HTTP-to-SOCKS5 bridge for `dial_iroh` (ADR-090 §5).
|
||||||
|
/// Feature-gated on `socks5`.
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
pub fn with_socks5_proxy(mut self, proxy: Socks5ProxyConfig) -> Self {
|
||||||
|
self.socks5 = Some(proxy);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AlknetClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for AlknetClient {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut configured: Vec<&str> = Vec::new();
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
if self.quinn.is_some() {
|
||||||
|
configured.push("quinn");
|
||||||
|
}
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
if self.tcp_connector.is_some() {
|
||||||
|
configured.push("tcp");
|
||||||
|
}
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
if self.iroh.is_some() {
|
||||||
|
configured.push("iroh");
|
||||||
|
}
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
if self.socks5.is_some() {
|
||||||
|
configured.push("socks5");
|
||||||
|
}
|
||||||
|
f.debug_struct("AlknetClient")
|
||||||
|
.field("transports", &configured)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//! `dial_iroh` — iroh dial, producing a `Connection`.
|
||||||
|
//!
|
||||||
|
//! Feature-gated on `iroh`. The iroh path does NOT use `TlsClientConfig` —
|
||||||
|
//! iroh has its own TLS (shares the `Ed25519SecretKey`, not the rustls config
|
||||||
|
//! — ADR-087 §3, ADR-089 §3). The local key is extracted from
|
||||||
|
//! `creds.local_identity`; the remote `EndpointId` is derived from
|
||||||
|
//! `creds.remote_identity.fingerprint`.
|
||||||
|
|
||||||
|
use alknet_core::credentials::ConnectionCredentials;
|
||||||
|
use alknet_core::types::Connection;
|
||||||
|
|
||||||
|
use crate::error::ClientDialError;
|
||||||
|
use crate::client::AlknetClient;
|
||||||
|
|
||||||
|
impl AlknetClient {
|
||||||
|
/// Iroh dial. Dials on `alpn` via the iroh endpoint. The iroh path
|
||||||
|
/// does NOT use `TlsClientConfig` — iroh has its own TLS (shares the
|
||||||
|
/// `Ed25519SecretKey`, not the rustls config — ADR-087 §3, ADR-089
|
||||||
|
/// §3). The local key is extracted from `creds.local_identity`; the
|
||||||
|
/// remote `EndpointId` is derived from `creds.remote_identity.fingerprint`
|
||||||
|
/// (`ed25519:<hex>` → `EndpointId::from_bytes`). The verifier is iroh's
|
||||||
|
/// `EndpointId` match (fingerprint pin by another name — ADR-034 §3).
|
||||||
|
/// An unknown iroh remote fails closed (no CA). Feature-gated on
|
||||||
|
/// `iroh`.
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub async fn dial_iroh(
|
||||||
|
&self,
|
||||||
|
alpn: &[u8],
|
||||||
|
creds: &ConnectionCredentials,
|
||||||
|
) -> Result<Connection, ClientDialError> {
|
||||||
|
let endpoint = self.iroh.as_ref().ok_or(ClientDialError::NoTransport {
|
||||||
|
transport: "iroh",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let node_id = match &creds.remote_identity {
|
||||||
|
Some(ri) => extract_iroh_endpoint_id(&ri.fingerprint).map_err(|e| {
|
||||||
|
ClientDialError::TlsConfig(alknet_tls::TlsError::Config(e))
|
||||||
|
})?,
|
||||||
|
None => {
|
||||||
|
return Err(ClientDialError::TlsConfig(alknet_tls::TlsError::Config(
|
||||||
|
"iroh requires a known remote (remote_identity must be Some); \
|
||||||
|
unknown iroh remotes fail closed (ADR-034 §3)"
|
||||||
|
.into(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let conn = endpoint
|
||||||
|
.connect(node_id, alpn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Connection::from_iroh(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract an `iroh::EndpointId` from a fingerprint string.
|
||||||
|
///
|
||||||
|
/// Supports two formats:
|
||||||
|
/// - `"ed25519:<hex>"` — raw Ed25519 public key (64 hex chars)
|
||||||
|
/// - `"SHA256:<base64>"` — SHA-256 hash of the cert (for X.509; not valid for iroh)
|
||||||
|
///
|
||||||
|
/// For iroh, only the `ed25519:` prefix is valid — iroh uses Ed25519 keys.
|
||||||
|
fn extract_iroh_endpoint_id(fingerprint: &str) -> Result<iroh::EndpointId, String> {
|
||||||
|
if let Some(hex_str) = fingerprint.strip_prefix("ed25519:") {
|
||||||
|
let bytes =
|
||||||
|
hex::decode(hex_str).map_err(|e| format!("invalid ed25519 fingerprint hex: {e}"))?;
|
||||||
|
if bytes.len() != 32 {
|
||||||
|
return Err(format!(
|
||||||
|
"invalid ed25519 fingerprint length: expected 32 bytes, got {}",
|
||||||
|
bytes.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let arr: [u8; 32] = bytes
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| "invalid ed25519 fingerprint length".to_string())?;
|
||||||
|
iroh::EndpointId::from_bytes(&arr)
|
||||||
|
.map_err(|e| format!("invalid iroh EndpointId: {e}"))
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"iroh requires an ed25519: fingerprint, got: {}",
|
||||||
|
fingerprint
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//! Dial methods for `AlknetClient` — one per transport.
|
||||||
|
//!
|
||||||
|
//! Each dial method is feature-gated on the corresponding transport feature.
|
||||||
|
//! All three are unified on `&ConnectionCredentials` (ADR-091) and return a
|
||||||
|
//! `Connection` for protocol take-overs to consume.
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub mod quinn;
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
pub mod tcp_tls;
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub mod iroh;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
//! `dial_quic` — QUIC dial via quinn, producing a `Connection`.
|
||||||
|
//!
|
||||||
|
//! Feature-gated on `quinn`. Builds a `TlsClientConfig` from
|
||||||
|
//! `ConnectionCredentials`, constructs a `quinn::ClientConfig`, dials
|
||||||
|
//! `addr` on `alpn`, and returns a `Connection` via
|
||||||
|
//! `Connection::from_quinn_with_alpn`.
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use alknet_core::credentials::ConnectionCredentials;
|
||||||
|
use alknet_core::types::Connection;
|
||||||
|
use alknet_tls::client::TlsClientConfig;
|
||||||
|
|
||||||
|
use crate::error::ClientDialError;
|
||||||
|
use crate::client::AlknetClient;
|
||||||
|
|
||||||
|
impl AlknetClient {
|
||||||
|
/// QUIC dial. Builds a `TlsClientConfig` from `creds`
|
||||||
|
/// (ADR-034 verifier selection + ADR-084 provider), dials `addr`
|
||||||
|
/// on `alpn`, returns a `Connection` via
|
||||||
|
/// `Connection::from_quinn_with_alpn`. The `server_name` is the
|
||||||
|
/// TLS SNI / name (for X.509; ignored for raw-key pinning).
|
||||||
|
/// Feature-gated on `quinn`.
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub async fn dial_quic(
|
||||||
|
&self,
|
||||||
|
addr: SocketAddr,
|
||||||
|
server_name: &str,
|
||||||
|
alpn: &[u8],
|
||||||
|
creds: &ConnectionCredentials,
|
||||||
|
) -> Result<Connection, ClientDialError> {
|
||||||
|
let tls_config = TlsClientConfig::new(creds, alpn)?;
|
||||||
|
let client_config = tls_config.for_quinn()?;
|
||||||
|
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
let conn = if let Some(proxy) = &self.socks5 {
|
||||||
|
let socket = crate::socks5::Socks5UdpSocket::bind(proxy).await?;
|
||||||
|
let endpoint = quinn::Endpoint::new_with_abstract_socket(
|
||||||
|
quinn::EndpointConfig::default(),
|
||||||
|
None,
|
||||||
|
Arc::new(socket),
|
||||||
|
Arc::new(quinn::TokioRuntime),
|
||||||
|
)
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
endpoint
|
||||||
|
.connect_with(client_config, addr, server_name)
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?
|
||||||
|
} else {
|
||||||
|
let endpoint = self
|
||||||
|
.quinn
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ClientDialError::NoTransport {
|
||||||
|
transport: "quinn",
|
||||||
|
})?;
|
||||||
|
endpoint
|
||||||
|
.connect_with(client_config, addr, server_name)
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "socks5"))]
|
||||||
|
let conn = {
|
||||||
|
let endpoint = self
|
||||||
|
.quinn
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ClientDialError::NoTransport {
|
||||||
|
transport: "quinn",
|
||||||
|
})?;
|
||||||
|
endpoint
|
||||||
|
.connect_with(client_config, addr, server_name)
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Connection::from_quinn_with_alpn(conn, alpn.to_vec()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
//! `dial_tcp_tls` — TCP+TLS dial via tokio-rustls, producing a `Connection`.
|
||||||
|
//!
|
||||||
|
//! Feature-gated on `tcp`. Builds a `TlsClientConfig` from
|
||||||
|
//! `ConnectionCredentials`, connects a `TcpStream` to `addr`, wraps with
|
||||||
|
//! `TlsConnector` using `host` as the SNI, and returns a `Connection` via
|
||||||
|
//! `Connection::from_bidi` (ADR-065).
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use alknet_core::credentials::ConnectionCredentials;
|
||||||
|
use alknet_core::types::Connection;
|
||||||
|
use alknet_tls::client::TlsClientConfig;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
|
use crate::error::ClientDialError;
|
||||||
|
use crate::client::AlknetClient;
|
||||||
|
|
||||||
|
impl AlknetClient {
|
||||||
|
/// TCP+TLS dial. Builds a `TlsClientConfig` from `creds`,
|
||||||
|
/// connects a `TcpStream` to `addr`, wraps with `TlsConnector`
|
||||||
|
/// using `host` as the SNI, returns a `Connection` via
|
||||||
|
/// `Connection::from_bidi` (ADR-065). Feature-gated on `tcp`.
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
pub async fn dial_tcp_tls(
|
||||||
|
&self,
|
||||||
|
host: &str,
|
||||||
|
addr: SocketAddr,
|
||||||
|
alpn: &[u8],
|
||||||
|
creds: &ConnectionCredentials,
|
||||||
|
) -> Result<Connection, ClientDialError> {
|
||||||
|
let tls_config = TlsClientConfig::new(creds, alpn)?;
|
||||||
|
|
||||||
|
let connector = match &self.tcp_connector {
|
||||||
|
Some(c) => c.clone(),
|
||||||
|
None => {
|
||||||
|
let rustls_config = Arc::new(tls_config.into_rustls_config());
|
||||||
|
tokio_rustls::TlsConnector::from(rustls_config)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
let tls_stream = if let Some(proxy) = &self.socks5 {
|
||||||
|
let mut tcp = TcpStream::connect(proxy.addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
crate::socks5::socks5_connect(&mut tcp, proxy, addr)
|
||||||
|
.await
|
||||||
|
.map_err(ClientDialError::Proxy)?;
|
||||||
|
let server_name: rustls::pki_types::ServerName = host
|
||||||
|
.to_owned()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e: rustls::pki_types::InvalidDnsNameError| {
|
||||||
|
ClientDialError::Connect(e.to_string())
|
||||||
|
})?;
|
||||||
|
connector
|
||||||
|
.connect(server_name, tcp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Handshake(e.to_string()))?
|
||||||
|
} else {
|
||||||
|
let tcp_stream = TcpStream::connect(addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
let server_name: rustls::pki_types::ServerName = host
|
||||||
|
.to_owned()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e: rustls::pki_types::InvalidDnsNameError| {
|
||||||
|
ClientDialError::Connect(e.to_string())
|
||||||
|
})?;
|
||||||
|
connector
|
||||||
|
.connect(server_name, tcp_stream)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Handshake(e.to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "socks5"))]
|
||||||
|
let tls_stream = {
|
||||||
|
let tcp_stream = TcpStream::connect(addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
let server_name: rustls::pki_types::ServerName = host
|
||||||
|
.to_owned()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e: rustls::pki_types::InvalidDnsNameError| {
|
||||||
|
ClientDialError::Connect(e.to_string())
|
||||||
|
})?;
|
||||||
|
connector
|
||||||
|
.connect(server_name, tcp_stream)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Handshake(e.to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Connection::from_bidi(
|
||||||
|
tls_stream,
|
||||||
|
alpn.to_vec(),
|
||||||
|
Some(addr),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//! `ClientDialError` — error type for all three dial methods.
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Errors produced by `AlknetClient` dial methods.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ClientDialError {
|
||||||
|
/// TLS config construction failure — `TlsClientConfig::new` failed
|
||||||
|
/// (verifier build, cert load, provider init). Wraps `TlsError`
|
||||||
|
/// from alknet-tls.
|
||||||
|
#[error("TLS config construction: {0}")]
|
||||||
|
TlsConfig(#[from] alknet_tls::TlsError),
|
||||||
|
|
||||||
|
/// Transport connect failure — quinn connect, TcpStream::connect,
|
||||||
|
/// or iroh connect. The transport's own error type, stringified.
|
||||||
|
#[error("transport connect: {0}")]
|
||||||
|
Connect(String),
|
||||||
|
|
||||||
|
/// TLS handshake failure — the handshake started but failed
|
||||||
|
/// (rejected cert, ALPN mismatch, unknown raw-key remote
|
||||||
|
/// fail-closed). Distinct from TlsConfig (which is pre-handshake).
|
||||||
|
#[error("TLS handshake: {0}")]
|
||||||
|
Handshake(String),
|
||||||
|
|
||||||
|
/// No transport handle configured for the requested dial — e.g.,
|
||||||
|
/// `dial_quic` called but `with_quinn` was not set.
|
||||||
|
#[error("no transport handle configured for {transport}")]
|
||||||
|
NoTransport { transport: &'static str },
|
||||||
|
|
||||||
|
/// SOCKS5 proxy failure — handshake rejected, UDP ASSOCIATE
|
||||||
|
/// unsupported, auth failed, or the proxy closed the control
|
||||||
|
/// connection (ADR-090). The dial did not reach the remote; the
|
||||||
|
/// caller decides whether to fall back to a direct dial or
|
||||||
|
/// surface the error. The dial never silently falls back — that
|
||||||
|
/// would defeat the privacy posture.
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
#[error("SOCKS5 proxy: {0}")]
|
||||||
|
Proxy(String),
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//! alknet-client: Native client dial seam — multi-transport dialer that
|
||||||
|
//! produces `Connection`s for protocol take-overs.
|
||||||
|
//!
|
||||||
|
//! `AlknetClient` is the client-side analogue of `AlknetEndpoint`: a
|
||||||
|
//! multi-transport dialer that takes pre-built transport handles (quinn,
|
||||||
|
//! TCP+TLS, iroh), dials a remote `AlknetEndpoint` on a chosen ALPN, and
|
||||||
|
//! produces a `Connection`. The protocol take-overs
|
||||||
|
//! (`CallClient::spawn_dispatch`, `ChannelClient::from_connection`)
|
||||||
|
//! consume the `Connection` — the dial is below the protocol.
|
||||||
|
//!
|
||||||
|
//! An optional SOCKS5 proxy (ADR-090) routes the dials through a proxy
|
||||||
|
//! to hide the client's real IP from the hub.
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
pub mod dial;
|
||||||
|
pub mod error;
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
pub mod socks5;
|
||||||
|
|
||||||
|
pub use client::AlknetClient;
|
||||||
|
pub use error::ClientDialError;
|
||||||
|
#[cfg(feature = "socks5")]
|
||||||
|
pub use socks5::{Socks5Credentials, Socks5ProxyConfig};
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
//! SOCKS5 proxy support for `AlknetClient` (ADR-090).
|
||||||
|
//!
|
||||||
|
//! When a proxy is configured via `with_socks5_proxy`, the rustls dials
|
||||||
|
//! route their transport through the proxy — the hub sees the proxy's IP,
|
||||||
|
//! not the client's.
|
||||||
|
//!
|
||||||
|
//! Feature-gated on `socks5`. The `Socks5UdpSocket` additionally requires
|
||||||
|
//! the `quinn` feature (it implements `quinn::AsyncUdpSocket`).
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
use std::io;
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
use std::pin::Pin;
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
use std::task::{Context, Poll, Waker};
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
use crate::error::ClientDialError;
|
||||||
|
|
||||||
|
/// Configuration for a SOCKS5 proxy (ADR-090).
|
||||||
|
///
|
||||||
|
/// When set on `AlknetClient` via `with_socks5_proxy`, all rustls dials
|
||||||
|
/// route their transport through this proxy: UDP ASSOCIATE for `dial_quic`,
|
||||||
|
/// CONNECT for `dial_tcp_tls`. The proxy config comes from `Capabilities` /
|
||||||
|
/// the assembly layer (ADR-014), never from environment variables.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Socks5ProxyConfig {
|
||||||
|
/// The proxy's TCP address (where the SOCKS5 control connection
|
||||||
|
/// connects). For UDP ASSOCIATE (the QUIC dial), the proxy replies
|
||||||
|
/// with a UDP relay address that may differ; the dial uses that.
|
||||||
|
pub addr: SocketAddr,
|
||||||
|
/// Optional username/password auth (RFC 1929). None = no-auth.
|
||||||
|
pub credentials: Option<Socks5Credentials>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SOCKS5 username/password credentials (RFC 1929).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Socks5Credentials {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `quinn::AsyncUdpSocket` implementation that tunnels QUIC datagrams
|
||||||
|
/// through a SOCKS5 UDP ASSOCIATE tunnel.
|
||||||
|
///
|
||||||
|
/// The implementation follows the pattern validated by the quinn-proxy PoC
|
||||||
|
/// (`docs/research/quinn-quic-proxy/findings.md`).
|
||||||
|
///
|
||||||
|
/// Requires both `socks5` and `quinn` features.
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub struct Socks5UdpSocket {
|
||||||
|
socket: std::net::UdpSocket,
|
||||||
|
relay_addr: SocketAddr,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
_control: TcpStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
impl Socks5UdpSocket {
|
||||||
|
/// Perform the SOCKS5 UDP ASSOCIATE handshake and return a socket
|
||||||
|
/// that tunnels QUIC datagrams through the proxy.
|
||||||
|
pub async fn bind(proxy: &Socks5ProxyConfig) -> Result<Self, ClientDialError> {
|
||||||
|
let mut control = TcpStream::connect(proxy.addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
|
||||||
|
socks5_handshake(&mut control, proxy).await?;
|
||||||
|
|
||||||
|
let relay_addr = socks5_udp_associate(&mut control).await?;
|
||||||
|
|
||||||
|
let socket = std::net::UdpSocket::bind("0.0.0.0:0")
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
socket
|
||||||
|
.set_nonblocking(true)
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
let local_addr = socket
|
||||||
|
.local_addr()
|
||||||
|
.map_err(|e| ClientDialError::Connect(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
socket,
|
||||||
|
relay_addr,
|
||||||
|
local_addr,
|
||||||
|
_control: control,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
impl quinn::AsyncUdpSocket for Socks5UdpSocket {
|
||||||
|
fn create_io_poller(self: Arc<Self>) -> Pin<Box<dyn quinn::UdpPoller>> {
|
||||||
|
Box::pin(UdpPollerImpl {
|
||||||
|
socket: self.socket.try_clone().ok(),
|
||||||
|
waker: Mutex::new(None),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_send(&self, transmit: &quinn::udp::Transmit) -> io::Result<()> {
|
||||||
|
let mut buf = Vec::with_capacity(10 + transmit.contents.len());
|
||||||
|
buf.extend_from_slice(&[0u8, 0, 0]);
|
||||||
|
match transmit.destination {
|
||||||
|
SocketAddr::V4(addr) => {
|
||||||
|
buf.push(0x01);
|
||||||
|
buf.extend_from_slice(&addr.ip().octets());
|
||||||
|
buf.extend_from_slice(&addr.port().to_be_bytes());
|
||||||
|
}
|
||||||
|
SocketAddr::V6(addr) => {
|
||||||
|
buf.push(0x04);
|
||||||
|
buf.extend_from_slice(&addr.ip().octets());
|
||||||
|
buf.extend_from_slice(&addr.port().to_be_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.extend_from_slice(transmit.contents);
|
||||||
|
|
||||||
|
let sent = self.socket.send_to(&buf, self.relay_addr)?;
|
||||||
|
if sent < buf.len() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::WouldBlock,
|
||||||
|
"partial send",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_recv(
|
||||||
|
&self,
|
||||||
|
_cx: &mut Context,
|
||||||
|
bufs: &mut [io::IoSliceMut<'_>],
|
||||||
|
meta: &mut [quinn::udp::RecvMeta],
|
||||||
|
) -> Poll<io::Result<usize>> {
|
||||||
|
let mut buf = [0u8; 65536];
|
||||||
|
match self.socket.recv_from(&mut buf) {
|
||||||
|
Ok((n, _src)) => {
|
||||||
|
if n < 10 {
|
||||||
|
return Poll::Ready(Ok(0));
|
||||||
|
}
|
||||||
|
let header_end = 3;
|
||||||
|
let atyp = buf[header_end];
|
||||||
|
let addr_len: usize = match atyp {
|
||||||
|
0x01 => 4,
|
||||||
|
0x04 => 16,
|
||||||
|
_ => return Poll::Ready(Ok(0)),
|
||||||
|
};
|
||||||
|
let payload_start = header_end + 1 + addr_len + 2;
|
||||||
|
if n < payload_start {
|
||||||
|
return Poll::Ready(Ok(0));
|
||||||
|
}
|
||||||
|
let payload = &buf[payload_start..n];
|
||||||
|
let copy_len = payload.len().min(bufs.iter().map(|b| b.len()).sum());
|
||||||
|
let mut offset = 0;
|
||||||
|
for b in bufs.iter_mut() {
|
||||||
|
let end = (offset + b.len()).min(copy_len);
|
||||||
|
if offset < end {
|
||||||
|
b.copy_from_slice(&payload[offset..end]);
|
||||||
|
}
|
||||||
|
offset = end;
|
||||||
|
if offset >= copy_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meta[0] = quinn::udp::RecvMeta {
|
||||||
|
len: copy_len,
|
||||||
|
stride: copy_len,
|
||||||
|
addr: self.relay_addr,
|
||||||
|
ecn: None,
|
||||||
|
dst_ip: None,
|
||||||
|
};
|
||||||
|
Poll::Ready(Ok(1))
|
||||||
|
}
|
||||||
|
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
Err(e) => Poll::Ready(Err(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||||
|
Ok(self.local_addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn may_fragment(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
struct UdpPollerImpl {
|
||||||
|
socket: Option<std::net::UdpSocket>,
|
||||||
|
waker: Mutex<Option<Waker>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
impl std::fmt::Debug for UdpPollerImpl {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("UdpPollerImpl").finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
impl quinn::UdpPoller for UdpPollerImpl {
|
||||||
|
fn poll_writable(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
|
||||||
|
if let Some(ref socket) = self.socket {
|
||||||
|
match socket.send_to(&[], socket.local_addr().ok().unwrap_or_else(|| "0.0.0.0:0".parse().unwrap())) {
|
||||||
|
Ok(_) => Poll::Ready(Ok(())),
|
||||||
|
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
*self.waker.lock().unwrap() = Some(cx.waker().clone());
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
Err(e) => Poll::Ready(Err(e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
impl std::fmt::Debug for Socks5UdpSocket {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Socks5UdpSocket")
|
||||||
|
.field("relay_addr", &self.relay_addr)
|
||||||
|
.field("local_addr", &self.local_addr)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the SOCKS5 handshake (greeting + auth).
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
async fn socks5_handshake(
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
proxy: &Socks5ProxyConfig,
|
||||||
|
) -> Result<(), ClientDialError> {
|
||||||
|
if let Some(creds) = &proxy.credentials {
|
||||||
|
stream
|
||||||
|
.write_all(&[0x05, 0x01, 0x02])
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
let mut resp = [0u8; 2];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
if resp[0] != 0x05 || resp[1] != 0x02 {
|
||||||
|
return Err(ClientDialError::Proxy(
|
||||||
|
"SOCKS5 server does not support username/password auth".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut auth_msg = Vec::with_capacity(3 + creds.username.len() + creds.password.len());
|
||||||
|
auth_msg.push(0x01);
|
||||||
|
auth_msg.push(creds.username.len() as u8);
|
||||||
|
auth_msg.extend_from_slice(creds.username.as_bytes());
|
||||||
|
auth_msg.push(creds.password.len() as u8);
|
||||||
|
auth_msg.extend_from_slice(creds.password.as_bytes());
|
||||||
|
stream
|
||||||
|
.write_all(&auth_msg)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
let mut auth_resp = [0u8; 2];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut auth_resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
if auth_resp[1] != 0x00 {
|
||||||
|
return Err(ClientDialError::Proxy(
|
||||||
|
"SOCKS5 username/password authentication failed".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stream
|
||||||
|
.write_all(&[0x05, 0x01, 0x00])
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
let mut resp = [0u8; 2];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
if resp[0] != 0x05 || resp[1] != 0x00 {
|
||||||
|
return Err(ClientDialError::Proxy(
|
||||||
|
"SOCKS5 server rejected no-auth method".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the SOCKS5 UDP ASSOCIATE request and return the relay address.
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
async fn socks5_udp_associate(stream: &mut TcpStream) -> Result<SocketAddr, ClientDialError> {
|
||||||
|
let req = vec![0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0];
|
||||||
|
stream
|
||||||
|
.write_all(&req)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut resp = [0u8; 10];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
|
||||||
|
if resp[0] != 0x05 {
|
||||||
|
return Err(ClientDialError::Proxy("invalid SOCKS5 version in reply".into()));
|
||||||
|
}
|
||||||
|
if resp[1] != 0x00 {
|
||||||
|
return Err(ClientDialError::Proxy(format!(
|
||||||
|
"SOCKS5 UDP ASSOCIATE rejected with code {}",
|
||||||
|
resp[1]
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bind_port = u16::from_be_bytes([resp[8], resp[9]]);
|
||||||
|
let bind_addr = match resp[3] {
|
||||||
|
0x01 => {
|
||||||
|
let mut addr = [0u8; 4];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
SocketAddr::new(std::net::Ipv4Addr::from(addr).into(), bind_port)
|
||||||
|
}
|
||||||
|
0x04 => {
|
||||||
|
let mut addr = [0u8; 16];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientDialError::Proxy(e.to_string()))?;
|
||||||
|
SocketAddr::new(std::net::Ipv6Addr::from(addr).into(), bind_port)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(ClientDialError::Proxy(format!(
|
||||||
|
"unsupported address type in UDP ASSOCIATE reply: {}",
|
||||||
|
resp[3]
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(bind_addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the SOCKS5 CONNECT handshake to the target address.
|
||||||
|
pub async fn socks5_connect(
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
proxy: &Socks5ProxyConfig,
|
||||||
|
target: SocketAddr,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
socks5_handshake_connect(stream, proxy).await?;
|
||||||
|
|
||||||
|
let mut req = vec![0x05, 0x01, 0x00];
|
||||||
|
match target {
|
||||||
|
SocketAddr::V4(addr) => {
|
||||||
|
req.push(0x01);
|
||||||
|
req.extend_from_slice(&addr.ip().octets());
|
||||||
|
req.extend_from_slice(&addr.port().to_be_bytes());
|
||||||
|
}
|
||||||
|
SocketAddr::V6(addr) => {
|
||||||
|
req.push(0x04);
|
||||||
|
req.extend_from_slice(&addr.ip().octets());
|
||||||
|
req.extend_from_slice(&addr.port().to_be_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream
|
||||||
|
.write_all(&req)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 CONNECT write: {e}"))?;
|
||||||
|
|
||||||
|
let mut resp = [0u8; 10];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 CONNECT read: {e}"))?;
|
||||||
|
|
||||||
|
if resp[0] != 0x05 {
|
||||||
|
return Err("invalid SOCKS5 version in CONNECT reply".into());
|
||||||
|
}
|
||||||
|
if resp[1] != 0x00 {
|
||||||
|
return Err(format!("SOCKS5 CONNECT rejected with code {}", resp[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
match resp[3] {
|
||||||
|
0x01 => {
|
||||||
|
let mut _addr = [0u8; 4];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut _addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 CONNECT bind addr read: {e}"))?;
|
||||||
|
}
|
||||||
|
0x04 => {
|
||||||
|
let mut _addr = [0u8; 16];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut _addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 CONNECT bind addr read: {e}"))?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn socks5_handshake_connect(
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
proxy: &Socks5ProxyConfig,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(creds) = &proxy.credentials {
|
||||||
|
stream
|
||||||
|
.write_all(&[0x05, 0x01, 0x02])
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 greeting write: {e}"))?;
|
||||||
|
let mut resp = [0u8; 2];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 greeting read: {e}"))?;
|
||||||
|
if resp[0] != 0x05 || resp[1] != 0x02 {
|
||||||
|
return Err("SOCKS5 server does not support username/password auth".into());
|
||||||
|
}
|
||||||
|
let mut auth_msg = Vec::with_capacity(3 + creds.username.len() + creds.password.len());
|
||||||
|
auth_msg.push(0x01);
|
||||||
|
auth_msg.push(creds.username.len() as u8);
|
||||||
|
auth_msg.extend_from_slice(creds.username.as_bytes());
|
||||||
|
auth_msg.push(creds.password.len() as u8);
|
||||||
|
auth_msg.extend_from_slice(creds.password.as_bytes());
|
||||||
|
stream
|
||||||
|
.write_all(&auth_msg)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 auth write: {e}"))?;
|
||||||
|
let mut auth_resp = [0u8; 2];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut auth_resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 auth read: {e}"))?;
|
||||||
|
if auth_resp[1] != 0x00 {
|
||||||
|
return Err("SOCKS5 username/password authentication failed".into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stream
|
||||||
|
.write_all(&[0x05, 0x01, 0x00])
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 greeting write: {e}"))?;
|
||||||
|
let mut resp = [0u8; 2];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut resp)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("SOCKS5 greeting read: {e}"))?;
|
||||||
|
if resp[0] != 0x05 || resp[1] != 0x00 {
|
||||||
|
return Err("SOCKS5 server rejected no-auth method".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -48,6 +48,12 @@ impl TlsClientConfig {
|
|||||||
.map_err(|e| TlsError::Config(e.to_string()))?,
|
.map_err(|e| TlsError::Config(e.to_string()))?,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consume the config and return the inner `rustls::ClientConfig`.
|
||||||
|
/// Used by `dial_tcp_tls` to build a `TlsConnector`.
|
||||||
|
pub fn into_rustls_config(self) -> rustls::ClientConfig {
|
||||||
|
self.rustls_config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the client-auth cert resolver that presents the local node's TLS
|
/// Build the client-auth cert resolver that presents the local node's TLS
|
||||||
|
|||||||
Reference in new issue
Block a user