Files
alknet/tasks/client/client-core.md
T
deepseek-v4-pro 83a94ec03f chore(client): mark all client tasks as completed
Phase 3 complete: alknet-client crate implemented with:
- AlknetClient struct with builder methods (with_quinn, with_tcp_tls, with_iroh, with_socks5_proxy)
- ClientDialError enum with 5 variants
- dial_quic, dial_tcp_tls, dial_iroh methods
- SOCKS5 proxy support (Socks5ProxyConfig, Socks5Credentials, Socks5UdpSocket)
- Unit tests (18) + integration test (3)
- All feature combos compile and test clean
- Workspace build and tests pass
2026-07-17 12:49:50 +00:00

8.1 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
client/client-core Implement AlknetClient struct, new, and builder methods (with_quinn, with_tcp_tls, with_iroh, with_socks5_proxy) completed
client/error-type
moderate medium component implementation

Description

Phase 3, Task 3. Implement the AlknetClient struct and its builder methods in crates/alknet-client/src/client.rs. This is the central type — the client-side analogue of AlknetEndpoint. Holds pre-built transport handles, all optional — the client dials with whichever transport the remote endpoint type implies.

This is a fresh build against the ADR-089/090/091 shape, not a copy of the old CallClient::connect. The old connect() built transports internally from CallCredentials; the new AlknetClient receives pre-built transports via builder methods, mirroring AlknetEndpoint's builder pattern (ADR-083).

Target shape (per architecture spec)

use std::sync::Arc;

#[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")]
    quinn: Option<quinn::Endpoint>,
    #[cfg(feature = "tcp")]
    tcp_connector: Option<tokio_rustls::TlsConnector>,
    #[cfg(feature = "iroh")]
    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")]
    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()
    }
}

Key design decisions

  1. Builder pattern mirrors AlknetEndpoint: with_quinn, with_iroh, with_tcp_tls are the same builder method names as the server side (ADR-083). The assembly layer builds the transport handles and hands them to the client.

  2. new() takes no parameters: No StaticConfig, no TlsClientConfig, no credentials. The client receives pre-built transports. This is the same pattern as AlknetEndpoint::new().

  3. with_tcp_tls takes a TlsConnector: The assembly layer builds the TlsConnector from TlsClientConfig::for_tcp_tls() (or directly from a rustls::ClientConfig). The client does not build TLS configs — it receives pre-built connectors.

  4. with_socks5_proxy is a client-level setting: The proxy is set once on the client (all dials use it), not per-dial. The proxy is a client-level privacy posture, not a per-connection choice. Feature-gated on socks5.

  5. No connect() method: The old CallClient::connect() welded the dial into the protocol crate. AlknetClient has three separate dial methods (dial_quic, dial_tcp_tls, dial_iroh) — each is a separate task.

  6. No protocol take-over: The client produces a Connection; the caller hands it to CallClient::spawn_dispatch or ChannelClient::from_connection. AlknetClient does not spawn the dispatch loop.

What this does NOT include

  • The dial methods (dial_quic, dial_tcp_tls, dial_iroh) — separate tasks
  • The SOCKS5 proxy implementation (Socks5UdpSocket, HTTP-to-SOCKS5 bridge) — separate task
  • Socks5ProxyConfig / Socks5Credentials types — defined in socks5.rs (separate task)
  • Tests — separate task

Acceptance Criteria

  • AlknetClient struct defined in crates/alknet-client/src/client.rs
  • Fields: quinn (feature-gated), tcp_connector (feature-gated), iroh (feature-gated), socks5 (feature-gated)
  • AlknetClient::new() takes no parameters, initializes all fields to None
  • with_quinn(endpoint) builder method (feature-gated on quinn)
  • with_tcp_tls(connector) builder method (feature-gated on tcp)
  • with_iroh(endpoint) builder method (feature-gated on iroh)
  • with_socks5_proxy(proxy) builder method (feature-gated on socks5)
  • Default impl delegates to new()
  • Debug impl (manual or derived — lists which transports are configured, no transport internals)
  • No connect() method (the old welded dial is not replicated)
  • No StaticConfig parameter on new() (assembly layer reads it)
  • No TlsClientConfig construction (client receives pre-built connectors)
  • No dependency on alknet-call (dial is below the protocol)
  • Feature gates correct: quinn, tcp, iroh, socks5 each gate their respective fields/methods
  • cargo check -p alknet-client succeeds (all feature combos)
  • cargo clippy -p alknet-client succeeds with no warnings
  • cargo build --workspace still succeeds (old code untouched)

References

  • docs/architecture/crates/client/README.md — AlknetClient section (lines 100-153)
  • docs/architecture/decisions/089-alknetclient-native-dial-seam.md — ADR-089
  • docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md — ADR-090 §1-2
  • docs/architecture/decisions/091-connectioncredentials-decouple-dial-from-call.md — ADR-091
  • crates/alknet-endpoint/src/endpoint.rs — AlknetEndpoint builder pattern (reference)
  • crates/alknet-call/src/client/call_client.rs — old CallClient struct (lines 102-187, reference for what NOT to replicate)

Notes

This is the core structural task of Phase 3. The AlknetClient is built fresh against the ADR-089/090/091 shape — it's not a copy of the old CallClient. The key difference: the old connect() built transports internally from CallCredentials; the new client receives pre-built transports via builder methods. The dial methods are separate tasks. The old code in call_client.rs is NOT deleted — that's Phase 5.

Summary

To be filled on completion