feat(cli): select transport in config; server MultiServer + client dial handover
- aura-cli config gains [transport] (order + per-transport ports + obfuscate/ masquerade); server binds all enabled transports via MultiServer, client uses dial() with UDP->TCP->QUIC handover. Config examples updated; backward-compatible (defaults to udp,tcp,quic). 21 cli tests incl. a real-UDP-transport loopback. - docs/sing-box.md: integration approach note (process-bridge now; native Go outbound for phones, with crypto-library mapping + KAT requirement). - Normalize rustfmt across the v2 transport files (tcp/dial/udp contract). Whole workspace: 97 tests pass, clippy -D warnings clean, fmt clean. Deploy flow (pki init/issue-server/issue-client) validated with the release binary. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -51,3 +51,19 @@ cidr = "10.7.0.0/24"
|
|||||||
[mimicry]
|
[mimicry]
|
||||||
# Enable traffic padding to blend packet sizes into HTTPS buckets.
|
# Enable traffic padding to blend packet sizes into HTTPS buckets.
|
||||||
padding = false
|
padding = false
|
||||||
|
|
||||||
|
[transport]
|
||||||
|
# Fallback order tried left-to-right ("handover"): the first transport that connects wins. Aura's
|
||||||
|
# own UDP transport is primary; TCP/443 and QUIC (HTTP/3 mimicry) are fallbacks for networks that
|
||||||
|
# throttle or block plain UDP. Omitting this whole section uses ["udp","tcp","quic"] on 443/443/444.
|
||||||
|
order = ["udp", "tcp", "quic"]
|
||||||
|
# Per-transport server ports. The server IP comes from [client] server_addr above (its port there is
|
||||||
|
# ignored). The UDP transport and QUIC both ride UDP, so udp_port and quic_port MUST differ; TCP may
|
||||||
|
# reuse the UDP port number.
|
||||||
|
udp_port = 443
|
||||||
|
tcp_port = 443
|
||||||
|
quic_port = 444
|
||||||
|
# UDP: pad datagrams up to HTTPS size buckets to blur the on-wire size distribution.
|
||||||
|
obfuscate = true
|
||||||
|
# TCP: prepend a minimal HTTP/1.1 preamble (Host = [client] sni) so the open resembles plain HTTP.
|
||||||
|
masquerade = true
|
||||||
|
|||||||
@@ -30,3 +30,18 @@ dns = "10.7.0.1"
|
|||||||
sni = "cdn.example.com"
|
sni = "cdn.example.com"
|
||||||
# Enable traffic padding to blend packet sizes into HTTPS buckets.
|
# Enable traffic padding to blend packet sizes into HTTPS buckets.
|
||||||
padding = true
|
padding = true
|
||||||
|
|
||||||
|
[transport]
|
||||||
|
# Aura's own post-quantum transport runs over plain UDP (primary), with TCP/443 and QUIC (HTTP/3
|
||||||
|
# mimicry) as fallbacks. On the server, `order` selects exactly which transports are bound and
|
||||||
|
# accepted simultaneously. Omitting this whole section enables udp/tcp/quic on 443/443/444.
|
||||||
|
order = ["udp", "tcp", "quic"]
|
||||||
|
# The UDP transport and QUIC both ride UDP, so udp_port and quic_port MUST differ. TCP may reuse the
|
||||||
|
# UDP port number (different protocol). Ports bind on the IP from [server] listen above.
|
||||||
|
udp_port = 443
|
||||||
|
tcp_port = 443
|
||||||
|
quic_port = 444
|
||||||
|
# UDP: pad datagrams up to HTTPS size buckets to blur the on-wire size distribution.
|
||||||
|
obfuscate = true
|
||||||
|
# TCP: prepend a minimal HTTP/1.1 preamble (Host = [mimicry] sni) so the open resembles plain HTTP.
|
||||||
|
masquerade = true
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
//! 1. Load `client.toml`, read the `[pki]` PEM files, build [`aura_proto::ClientConfig`].
|
//! 1. Load `client.toml`, read the `[pki]` PEM files, build [`aura_proto::ClientConfig`].
|
||||||
//! 2. Build a shared [`RouteTable`] from `[tunnel.split]` (default action + direct/vpn CIDR rules);
|
//! 2. Build a shared [`RouteTable`] from `[tunnel.split]` (default action + direct/vpn CIDR rules);
|
||||||
//! record domain rules for resolution.
|
//! record domain rules for resolution.
|
||||||
//! 3. [`AuraClient::connect`] to `[client] server_addr`, presenting `[client] sni` as the outer
|
//! 3. [`aura_transport::dial`] the server, trying each transport in `[transport] order` (the
|
||||||
//! (mimicry) hostname.
|
//! UDP→TCP→QUIC "handover") until one connects; QUIC presents `[client] sni` as the outer
|
||||||
|
//! (mimicry) hostname and TCP uses it as the masquerade `Host`.
|
||||||
//! 4. Resolve any split-tunnel domain rules via [`AuraDns`] into host routes (best-effort).
|
//! 4. Resolve any split-tunnel domain rules via [`AuraDns`] into host routes (best-effort).
|
||||||
//! 5. Create the local TUN ([`AuraTun::create`]) on `[tunnel] local_ip/prefix` and run
|
//! 5. Create the local TUN ([`AuraTun::create`]) on `[tunnel] local_ip/prefix` and run
|
||||||
//! [`AuraRouter`] to bridge the TUN and the connection.
|
//! [`AuraRouter`] to bridge the TUN and the connection.
|
||||||
@@ -21,7 +22,7 @@ use std::path::Path;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use aura_transport::AuraClient;
|
use aura_transport::dial;
|
||||||
use aura_tunnel::{AuraDns, AuraRouter, AuraTun};
|
use aura_tunnel::{AuraDns, AuraRouter, AuraTun};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
@@ -31,18 +32,22 @@ use crate::config::ClientConfigFile;
|
|||||||
/// Entry point for `aura client --config <PATH>` (and optional `--admin-socket`).
|
/// Entry point for `aura client --config <PATH>` (and optional `--admin-socket`).
|
||||||
pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
||||||
let cfg = ClientConfigFile::load(config_path)?;
|
let cfg = ClientConfigFile::load(config_path)?;
|
||||||
let server_addr = cfg.server_socket_addr()?;
|
|
||||||
let local_ip = cfg.local_ip()?;
|
let local_ip = cfg.local_ip()?;
|
||||||
let proto_cfg = cfg.to_proto()?;
|
let proto_cfg = cfg.to_proto()?;
|
||||||
|
let dial_cfg = cfg.dial_config()?;
|
||||||
let (table, domains) = cfg.build_route_table()?;
|
let (table, domains) = cfg.build_route_table()?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
name = %cfg.client.name,
|
name = %cfg.client.name,
|
||||||
%server_addr,
|
server_addr = %cfg.client.server_addr,
|
||||||
sni = %cfg.client.sni,
|
sni = %cfg.client.sni,
|
||||||
%local_ip,
|
%local_ip,
|
||||||
dns = ?cfg.tunnel.dns,
|
dns = ?cfg.tunnel.dns,
|
||||||
mimicry_padding = cfg.mimicry.padding,
|
mimicry_padding = cfg.mimicry.padding,
|
||||||
|
transport_order = ?cfg.transport.order,
|
||||||
|
udp = ?dial_cfg.endpoints.udp,
|
||||||
|
tcp = ?dial_cfg.endpoints.tcp,
|
||||||
|
quic = ?dial_cfg.endpoints.quic,
|
||||||
"starting Aura client"
|
"starting Aura client"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -53,13 +58,17 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
|||||||
let routes = Arc::new(RwLock::new(table));
|
let routes = Arc::new(RwLock::new(table));
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
|
|
||||||
// Connect (outer QUIC + inner Aura mutual-auth handshake).
|
// Dial: try each transport in `[transport] order` (UDP→TCP→QUIC handover) until one connects.
|
||||||
let conn = AuraClient::connect(server_addr, &cfg.client.sni, proto_cfg)
|
// Each transport runs the inner Aura mutual-auth handshake; the winner is returned as a uniform
|
||||||
|
// `Arc<dyn PacketConnection>` along with which mode carried it. (The trait object does not surface
|
||||||
|
// the verified server CN; the server identity was already checked against `[client] sni` inside
|
||||||
|
// the handshake, so we record that as the peer for the admin/status mirror.)
|
||||||
|
let (conn, mode) = dial(proto_cfg, dial_cfg)
|
||||||
.await
|
.await
|
||||||
.context("connecting to Aura server")?;
|
.context("connecting to Aura server")?;
|
||||||
let peer = conn.peer_id().map(str::to_owned);
|
let peer = Some(cfg.client.sni.clone());
|
||||||
stats.set_peer_id(peer.clone());
|
stats.set_peer_id(peer.clone());
|
||||||
tracing::info!(peer = ?peer, "connected and authenticated to server");
|
tracing::info!(peer = ?peer, %mode, "connected and authenticated to server");
|
||||||
|
|
||||||
// Resolve split-tunnel domain rules into host routes (best-effort; failures are logged).
|
// Resolve split-tunnel domain rules into host routes (best-effort; failures are logged).
|
||||||
if !domains.is_empty() {
|
if !domains.is_empty() {
|
||||||
@@ -110,7 +119,7 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
|||||||
.context("creating TUN device (needs root)")?;
|
.context("creating TUN device (needs root)")?;
|
||||||
tracing::info!(tun = %cfg.tunnel.tun_name, "TUN device up; routing traffic");
|
tracing::info!(tun = %cfg.tunnel.tun_name, "TUN device up; routing traffic");
|
||||||
|
|
||||||
let router = AuraRouter::new(tun, routes, conn.into_dyn());
|
let router = AuraRouter::new(tun, routes, conn);
|
||||||
router.run().await.context("router run loop")?;
|
router.run().await.context("router run loop")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,11 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
use aura_transport::{DialConfig, Endpoints, TcpOpts, TransportMode, UdpOpts};
|
||||||
use aura_tunnel::{RouteAction, RouteTable};
|
use aura_tunnel::{RouteAction, RouteTable};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -34,6 +37,9 @@ pub struct ServerConfigFile {
|
|||||||
/// `[mimicry]` section: outer-TLS camouflage knobs.
|
/// `[mimicry]` section: outer-TLS camouflage knobs.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mimicry: ServerMimicrySection,
|
pub mimicry: ServerMimicrySection,
|
||||||
|
/// `[transport]` section: which transports to enable and their per-transport ports/options.
|
||||||
|
#[serde(default)]
|
||||||
|
pub transport: TransportSection,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `[server]` section.
|
/// `[server]` section.
|
||||||
@@ -87,6 +93,9 @@ pub struct ClientConfigFile {
|
|||||||
/// `[mimicry]` section: outer-TLS camouflage knobs.
|
/// `[mimicry]` section: outer-TLS camouflage knobs.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mimicry: ClientMimicrySection,
|
pub mimicry: ClientMimicrySection,
|
||||||
|
/// `[transport]` section: fallback order and per-transport ports/options.
|
||||||
|
#[serde(default)]
|
||||||
|
pub transport: TransportSection,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `[client]` section.
|
/// `[client]` section.
|
||||||
@@ -178,6 +187,91 @@ pub struct PkiSection {
|
|||||||
pub key: String,
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `[transport]` section shared by both config files: the set/order of transports and their ports.
|
||||||
|
///
|
||||||
|
/// Aura's primary transport is its own post-quantum protocol over **plain UDP**, with **TCP/443**
|
||||||
|
/// and **QUIC** (HTTP/3 mimicry) as fallbacks. This section maps directly onto
|
||||||
|
/// [`aura_transport::Endpoints`] (server) and [`aura_transport::DialConfig`] (client):
|
||||||
|
///
|
||||||
|
/// * On the **client**, `order` is the fallback order tried left-to-right ("handover"); the first
|
||||||
|
/// transport that connects wins.
|
||||||
|
/// * On the **server**, `order` selects exactly which transports are bound and accepted at once.
|
||||||
|
///
|
||||||
|
/// The UDP transport and QUIC both ride UDP, so they **must** use different ports (`udp_port` vs
|
||||||
|
/// `quic_port`); TCP may reuse the UDP port number (different protocol). Backwards compatible: when
|
||||||
|
/// the whole section is omitted, [`TransportSection::default`] enables `udp, tcp, quic` on the
|
||||||
|
/// standard ports, so pre-transport-v2 configs keep working.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct TransportSection {
|
||||||
|
/// Client fallback order; server enables exactly these. Strings: `"udp"`, `"tcp"`, `"quic"`.
|
||||||
|
pub order: Vec<String>,
|
||||||
|
/// Port for Aura's custom UDP transport. Must differ from `quic_port`.
|
||||||
|
pub udp_port: u16,
|
||||||
|
/// Port for the TCP fallback. May equal `udp_port` (different protocol).
|
||||||
|
pub tcp_port: u16,
|
||||||
|
/// Port for the QUIC fallback. Must differ from `udp_port`.
|
||||||
|
pub quic_port: u16,
|
||||||
|
/// UDP transport: pad datagrams up to HTTPS size buckets to blur on-wire sizes.
|
||||||
|
pub obfuscate: bool,
|
||||||
|
/// TCP transport: prepend a minimal HTTP/1.1 preamble so the open resembles plain HTTP.
|
||||||
|
pub masquerade: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TransportSection {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
order: default_transport_order(),
|
||||||
|
udp_port: 443,
|
||||||
|
tcp_port: 443,
|
||||||
|
quic_port: 444,
|
||||||
|
obfuscate: true,
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportSection {
|
||||||
|
/// Parse `order` into [`TransportMode`]s, rejecting unknown names and duplicates.
|
||||||
|
pub fn modes(&self) -> anyhow::Result<Vec<TransportMode>> {
|
||||||
|
let mut modes = Vec::with_capacity(self.order.len());
|
||||||
|
for raw in &self.order {
|
||||||
|
let mode = TransportMode::from_str(raw)
|
||||||
|
.map_err(|e| anyhow!("invalid [transport] order entry: {e}"))?;
|
||||||
|
if modes.contains(&mode) {
|
||||||
|
return Err(anyhow!("duplicate transport '{mode}' in [transport] order"));
|
||||||
|
}
|
||||||
|
modes.push(mode);
|
||||||
|
}
|
||||||
|
if modes.is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"[transport] order must list at least one transport"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// UDP and QUIC share the UDP socket layer, so they cannot collide on the same port.
|
||||||
|
if modes.contains(&TransportMode::Udp)
|
||||||
|
&& modes.contains(&TransportMode::Quic)
|
||||||
|
&& self.udp_port == self.quic_port
|
||||||
|
{
|
||||||
|
return Err(anyhow!(
|
||||||
|
"[transport] udp_port and quic_port must differ ({} used for both); \
|
||||||
|
the UDP transport and QUIC both use UDP",
|
||||||
|
self.udp_port
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(modes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The configured port for a given transport mode.
|
||||||
|
fn port_for(&self, mode: TransportMode) -> u16 {
|
||||||
|
match mode {
|
||||||
|
TransportMode::Udp => self.udp_port,
|
||||||
|
TransportMode::Tcp => self.tcp_port,
|
||||||
|
TransportMode::Quic => self.quic_port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- defaults -------------------------------------------------------------------------------
|
// ---- defaults -------------------------------------------------------------------------------
|
||||||
|
|
||||||
fn default_listen() -> String {
|
fn default_listen() -> String {
|
||||||
@@ -198,6 +292,11 @@ fn default_tun_name() -> String {
|
|||||||
fn default_split_default() -> String {
|
fn default_split_default() -> String {
|
||||||
"VPN".to_string()
|
"VPN".to_string()
|
||||||
}
|
}
|
||||||
|
/// Default transport set/order when `[transport]` (or its `order`) is omitted: UDP first, then the
|
||||||
|
/// TCP/443 and QUIC fallbacks. Keeps pre-transport-v2 configs working.
|
||||||
|
fn default_transport_order() -> Vec<String> {
|
||||||
|
vec!["udp".to_string(), "tcp".to_string(), "quic".to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
// ---- ~ expansion ----------------------------------------------------------------------------
|
// ---- ~ expansion ----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -274,6 +373,48 @@ impl ServerConfigFile {
|
|||||||
server_key_pem: read_pem(&self.pki.key)?,
|
server_key_pem: read_pem(&self.pki.key)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the per-transport [`Endpoints`] the [`aura_transport::MultiServer`] should bind.
|
||||||
|
///
|
||||||
|
/// The listen **IP** is reused from `[server] listen`; each enabled transport (from
|
||||||
|
/// `[transport] order`) gets its `udp_port` / `tcp_port` / `quic_port`. Transports not listed in
|
||||||
|
/// `order` are left `None` (disabled).
|
||||||
|
pub fn transport_endpoints(&self) -> anyhow::Result<Endpoints> {
|
||||||
|
let ip = self.listen_addr()?.ip();
|
||||||
|
let modes = self.transport.modes()?;
|
||||||
|
let mut endpoints = Endpoints::default();
|
||||||
|
for mode in modes {
|
||||||
|
let addr = SocketAddr::new(ip, self.transport.port_for(mode));
|
||||||
|
match mode {
|
||||||
|
TransportMode::Udp => endpoints.udp = Some(addr),
|
||||||
|
TransportMode::Tcp => endpoints.tcp = Some(addr),
|
||||||
|
TransportMode::Quic => endpoints.quic = Some(addr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(endpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the [`UdpOpts`] for the server's UDP transport from `[transport] obfuscate` (timeouts
|
||||||
|
/// keep their library defaults).
|
||||||
|
pub fn udp_opts(&self) -> UdpOpts {
|
||||||
|
UdpOpts {
|
||||||
|
obfuscate: self.transport.obfuscate,
|
||||||
|
..UdpOpts::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the [`TcpOpts`] for the server's TCP transport from `[transport] masquerade`; the
|
||||||
|
/// masquerade `Host` reuses the mimicry SNI when one is configured.
|
||||||
|
pub fn tcp_opts(&self) -> TcpOpts {
|
||||||
|
let mut opts = TcpOpts {
|
||||||
|
masquerade: self.transport.masquerade,
|
||||||
|
..TcpOpts::default()
|
||||||
|
};
|
||||||
|
if let Some(sni) = &self.mimicry.sni {
|
||||||
|
opts.host = sni.clone();
|
||||||
|
}
|
||||||
|
opts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientConfigFile {
|
impl ClientConfigFile {
|
||||||
@@ -305,6 +446,40 @@ impl ClientConfigFile {
|
|||||||
.with_context(|| format!("invalid [tunnel] local_ip '{}'", self.tunnel.local_ip))
|
.with_context(|| format!("invalid [tunnel] local_ip '{}'", self.tunnel.local_ip))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the [`DialConfig`] the client passes to [`aura_transport::dial`].
|
||||||
|
///
|
||||||
|
/// The server **IP** is taken from `[client] server_addr` (its port is ignored: each transport
|
||||||
|
/// uses its own port from `[transport]`). `order` becomes the fallback order, and the per-
|
||||||
|
/// transport options (UDP `obfuscate`, TCP `masquerade`/`host` and the QUIC SNI) come from
|
||||||
|
/// `[transport]` + `[client] sni`.
|
||||||
|
pub fn dial_config(&self) -> anyhow::Result<DialConfig> {
|
||||||
|
let ip = self.server_socket_addr()?.ip();
|
||||||
|
let order = self.transport.modes()?;
|
||||||
|
let mut endpoints = Endpoints::default();
|
||||||
|
for mode in &order {
|
||||||
|
let addr = SocketAddr::new(ip, self.transport.port_for(*mode));
|
||||||
|
match mode {
|
||||||
|
TransportMode::Udp => endpoints.udp = Some(addr),
|
||||||
|
TransportMode::Tcp => endpoints.tcp = Some(addr),
|
||||||
|
TransportMode::Quic => endpoints.quic = Some(addr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(DialConfig {
|
||||||
|
endpoints,
|
||||||
|
sni: self.client.sni.clone(),
|
||||||
|
order,
|
||||||
|
udp: UdpOpts {
|
||||||
|
obfuscate: self.transport.obfuscate,
|
||||||
|
..UdpOpts::default()
|
||||||
|
},
|
||||||
|
tcp: TcpOpts {
|
||||||
|
masquerade: self.transport.masquerade,
|
||||||
|
host: self.client.sni.clone(),
|
||||||
|
},
|
||||||
|
attempt_timeout: Duration::from_secs(8),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the `[pki]` PEM files and build an [`aura_proto::ClientConfig`].
|
/// Read the `[pki]` PEM files and build an [`aura_proto::ClientConfig`].
|
||||||
///
|
///
|
||||||
/// The inner-handshake `server_name` is taken from `[client] sni` so the SAN verified against
|
/// The inner-handshake `server_name` is taken from `[client] sni` so the SAN verified against
|
||||||
@@ -398,6 +573,14 @@ dns = "10.7.0.1"
|
|||||||
[mimicry]
|
[mimicry]
|
||||||
sni = "cdn.example.com"
|
sni = "cdn.example.com"
|
||||||
padding = true
|
padding = true
|
||||||
|
|
||||||
|
[transport]
|
||||||
|
order = ["udp", "tcp", "quic"]
|
||||||
|
udp_port = 4433
|
||||||
|
tcp_port = 4433
|
||||||
|
quic_port = 4434
|
||||||
|
obfuscate = true
|
||||||
|
masquerade = true
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const CLIENT_TOML: &str = r#"
|
const CLIENT_TOML: &str = r#"
|
||||||
@@ -434,6 +617,14 @@ cidr = "10.7.0.0/24"
|
|||||||
|
|
||||||
[mimicry]
|
[mimicry]
|
||||||
padding = false
|
padding = false
|
||||||
|
|
||||||
|
[transport]
|
||||||
|
order = ["tcp", "udp"]
|
||||||
|
udp_port = 4433
|
||||||
|
tcp_port = 4433
|
||||||
|
quic_port = 4434
|
||||||
|
obfuscate = false
|
||||||
|
masquerade = true
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -447,6 +638,18 @@ padding = false
|
|||||||
assert!(cfg.mimicry.padding);
|
assert!(cfg.mimicry.padding);
|
||||||
assert_eq!(cfg.mimicry.sni.as_deref(), Some("cdn.example.com"));
|
assert_eq!(cfg.mimicry.sni.as_deref(), Some("cdn.example.com"));
|
||||||
assert_eq!(cfg.pki.ca_cert, "/etc/aura/ca.crt");
|
assert_eq!(cfg.pki.ca_cert, "/etc/aura/ca.crt");
|
||||||
|
|
||||||
|
// [transport]: order + ports parse and the server endpoints reuse the listen IP.
|
||||||
|
assert_eq!(cfg.transport.order, vec!["udp", "tcp", "quic"]);
|
||||||
|
assert_eq!(cfg.transport.quic_port, 4434);
|
||||||
|
let eps = cfg.transport_endpoints().expect("server endpoints");
|
||||||
|
assert_eq!(eps.udp.unwrap().to_string(), "0.0.0.0:4433");
|
||||||
|
assert_eq!(eps.tcp.unwrap().to_string(), "0.0.0.0:4433");
|
||||||
|
assert_eq!(eps.quic.unwrap().to_string(), "0.0.0.0:4434");
|
||||||
|
assert!(cfg.udp_opts().obfuscate);
|
||||||
|
let tcp = cfg.tcp_opts();
|
||||||
|
assert!(tcp.masquerade);
|
||||||
|
assert_eq!(tcp.host, "cdn.example.com"); // reuses mimicry SNI
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -466,6 +669,17 @@ pool_cidr = "10.7.0.0/24"
|
|||||||
assert_eq!(cfg.server.workers, 1);
|
assert_eq!(cfg.server.workers, 1);
|
||||||
assert_eq!(cfg.tunnel.mtu, 1420);
|
assert_eq!(cfg.tunnel.mtu, 1420);
|
||||||
assert!(!cfg.mimicry.padding);
|
assert!(!cfg.mimicry.padding);
|
||||||
|
|
||||||
|
// Omitting [transport] yields the backward-compatible defaults (udp/tcp/quic on 443/443/444).
|
||||||
|
assert_eq!(cfg.transport.order, vec!["udp", "tcp", "quic"]);
|
||||||
|
assert_eq!(cfg.transport.udp_port, 443);
|
||||||
|
assert_eq!(cfg.transport.tcp_port, 443);
|
||||||
|
assert_eq!(cfg.transport.quic_port, 444);
|
||||||
|
assert!(cfg.transport.obfuscate);
|
||||||
|
assert!(cfg.transport.masquerade);
|
||||||
|
let eps = cfg.transport_endpoints().expect("default endpoints");
|
||||||
|
assert_eq!(eps.udp.unwrap().to_string(), "0.0.0.0:443");
|
||||||
|
assert_eq!(eps.quic.unwrap().to_string(), "0.0.0.0:444");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -481,6 +695,21 @@ pool_cidr = "10.7.0.0/24"
|
|||||||
assert_eq!(cfg.tunnel.prefix, 24);
|
assert_eq!(cfg.tunnel.prefix, 24);
|
||||||
assert_eq!(cfg.tunnel.split.direct.len(), 3);
|
assert_eq!(cfg.tunnel.split.direct.len(), 3);
|
||||||
assert_eq!(cfg.tunnel.split.vpn.len(), 1);
|
assert_eq!(cfg.tunnel.split.vpn.len(), 1);
|
||||||
|
|
||||||
|
// [transport]: the client dial config honors `order` and reuses the server IP per transport.
|
||||||
|
let dial = cfg.dial_config().expect("client dial config");
|
||||||
|
assert_eq!(
|
||||||
|
dial.order,
|
||||||
|
vec![TransportMode::Tcp, TransportMode::Udp] // as written in CLIENT_TOML
|
||||||
|
);
|
||||||
|
assert_eq!(dial.endpoints.tcp.unwrap().to_string(), "203.0.113.10:4433");
|
||||||
|
assert_eq!(dial.endpoints.udp.unwrap().to_string(), "203.0.113.10:4433");
|
||||||
|
// QUIC was not in `order`, so it is left unconfigured.
|
||||||
|
assert!(dial.endpoints.quic.is_none());
|
||||||
|
assert_eq!(dial.sni, "cdn.example.com");
|
||||||
|
assert!(!dial.udp.obfuscate);
|
||||||
|
assert!(dial.tcp.masquerade);
|
||||||
|
assert_eq!(dial.tcp.host, "cdn.example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -540,10 +769,16 @@ pool_cidr = "10.7.0.0/24"
|
|||||||
let s = ServerConfigFile::parse(&server).expect("server.toml.example parses");
|
let s = ServerConfigFile::parse(&server).expect("server.toml.example parses");
|
||||||
assert!(s.listen_addr().is_ok());
|
assert!(s.listen_addr().is_ok());
|
||||||
assert!(s.pool_network().is_ok());
|
assert!(s.pool_network().is_ok());
|
||||||
|
// The shipped [transport] section builds valid endpoints (udp_port != quic_port).
|
||||||
|
let eps = s
|
||||||
|
.transport_endpoints()
|
||||||
|
.expect("example server endpoints build");
|
||||||
|
assert!(eps.udp.is_some() && eps.tcp.is_some() && eps.quic.is_some());
|
||||||
|
|
||||||
let c = ClientConfigFile::parse(&client).expect("client.toml.example parses");
|
let c = ClientConfigFile::parse(&client).expect("client.toml.example parses");
|
||||||
assert!(c.server_socket_addr().is_ok());
|
assert!(c.server_socket_addr().is_ok());
|
||||||
assert!(c.local_ip().is_ok());
|
assert!(c.local_ip().is_ok());
|
||||||
|
assert!(c.dial_config().is_ok(), "example client dial config builds");
|
||||||
let (table, domains) = c
|
let (table, domains) = c
|
||||||
.build_route_table()
|
.build_route_table()
|
||||||
.expect("example split builds a route table");
|
.expect("example split builds a route table");
|
||||||
@@ -573,4 +808,102 @@ domain = "x.example.com"
|
|||||||
let cfg = ClientConfigFile::parse(bad).expect("parse");
|
let cfg = ClientConfigFile::parse(bad).expect("parse");
|
||||||
assert!(cfg.build_route_table().is_err());
|
assert!(cfg.build_route_table().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A client config with no `[transport]` section falls back to the udp→tcp→quic defaults so old
|
||||||
|
/// configs keep working.
|
||||||
|
#[test]
|
||||||
|
fn client_transport_defaults_when_omitted() {
|
||||||
|
let minimal = r#"
|
||||||
|
[client]
|
||||||
|
name = "x"
|
||||||
|
server_addr = "1.2.3.4:443"
|
||||||
|
sni = "a"
|
||||||
|
[pki]
|
||||||
|
ca_cert = "a"
|
||||||
|
cert = "b"
|
||||||
|
key = "c"
|
||||||
|
[tunnel]
|
||||||
|
local_ip = "10.7.0.2"
|
||||||
|
"#;
|
||||||
|
let cfg = ClientConfigFile::parse(minimal).expect("parse");
|
||||||
|
let dial = cfg.dial_config().expect("dial config");
|
||||||
|
assert_eq!(
|
||||||
|
dial.order,
|
||||||
|
vec![TransportMode::Udp, TransportMode::Tcp, TransportMode::Quic]
|
||||||
|
);
|
||||||
|
assert_eq!(dial.endpoints.udp.unwrap().to_string(), "1.2.3.4:443");
|
||||||
|
assert_eq!(dial.endpoints.tcp.unwrap().to_string(), "1.2.3.4:443");
|
||||||
|
assert_eq!(dial.endpoints.quic.unwrap().to_string(), "1.2.3.4:444");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UDP and QUIC share the UDP socket layer; configuring the same port for both must be rejected.
|
||||||
|
#[test]
|
||||||
|
fn rejects_udp_quic_port_collision() {
|
||||||
|
let bad = r#"
|
||||||
|
[server]
|
||||||
|
name = "edge"
|
||||||
|
[pki]
|
||||||
|
ca_cert = "a"
|
||||||
|
cert = "b"
|
||||||
|
key = "c"
|
||||||
|
[tunnel]
|
||||||
|
pool_cidr = "10.7.0.0/24"
|
||||||
|
[transport]
|
||||||
|
order = ["udp", "quic"]
|
||||||
|
udp_port = 443
|
||||||
|
quic_port = 443
|
||||||
|
"#;
|
||||||
|
let cfg = ServerConfigFile::parse(bad).expect("parse");
|
||||||
|
let err = cfg.transport_endpoints().unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
err.contains("udp_port") && err.contains("quic_port"),
|
||||||
|
"{err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sharing the UDP/QUIC port number is fine if only one of them is enabled.
|
||||||
|
#[test]
|
||||||
|
fn allows_shared_port_when_quic_disabled() {
|
||||||
|
let ok = r#"
|
||||||
|
[server]
|
||||||
|
name = "edge"
|
||||||
|
[pki]
|
||||||
|
ca_cert = "a"
|
||||||
|
cert = "b"
|
||||||
|
key = "c"
|
||||||
|
[tunnel]
|
||||||
|
pool_cidr = "10.7.0.0/24"
|
||||||
|
[transport]
|
||||||
|
order = ["udp", "tcp"]
|
||||||
|
udp_port = 443
|
||||||
|
tcp_port = 443
|
||||||
|
quic_port = 443
|
||||||
|
"#;
|
||||||
|
let cfg = ServerConfigFile::parse(ok).expect("parse");
|
||||||
|
let eps = cfg.transport_endpoints().expect("endpoints");
|
||||||
|
assert!(eps.udp.is_some());
|
||||||
|
assert!(eps.tcp.is_some());
|
||||||
|
assert!(eps.quic.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unknown transport name in `order` is a hard error (not silently dropped).
|
||||||
|
#[test]
|
||||||
|
fn rejects_unknown_transport_name() {
|
||||||
|
let bad = r#"
|
||||||
|
[client]
|
||||||
|
name = "x"
|
||||||
|
server_addr = "1.2.3.4:443"
|
||||||
|
sni = "a"
|
||||||
|
[pki]
|
||||||
|
ca_cert = "a"
|
||||||
|
cert = "b"
|
||||||
|
key = "c"
|
||||||
|
[tunnel]
|
||||||
|
local_ip = "10.7.0.2"
|
||||||
|
[transport]
|
||||||
|
order = ["udp", "smoke-signals"]
|
||||||
|
"#;
|
||||||
|
let cfg = ClientConfigFile::parse(bad).expect("parse");
|
||||||
|
assert!(cfg.dial_config().is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ enum Command {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Pki(PkiCommand),
|
Pki(PkiCommand),
|
||||||
|
|
||||||
/// Run the Aura VPN server (binds QUIC, accepts clients; needs root for the TUN device).
|
/// Run the Aura VPN server (binds the configured transports, accepts clients; needs root for TUN).
|
||||||
Server(ServerArgs),
|
Server(ServerArgs),
|
||||||
|
|
||||||
/// Run the Aura VPN client (connects to a server; needs root for the TUN device).
|
/// Run the Aura VPN client (connects to a server; needs root for the TUN device).
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
//! `aura server`: bind an [`AuraServer`], accept connections, and pump packets to a server-side TUN.
|
//! `aura server`: bind a [`MultiServer`], accept connections on every enabled transport, and pump
|
||||||
|
//! packets to a server-side TUN.
|
||||||
//!
|
//!
|
||||||
//! ## v1 data path
|
//! ## v1 data path
|
||||||
//! 1. Load `server.toml`, read the `[pki]` PEM files, build [`aura_proto::ServerConfig`].
|
//! 1. Load `server.toml`, read the `[pki]` PEM files, build [`aura_proto::ServerConfig`].
|
||||||
//! 2. [`AuraServer::bind`] on `[server] listen` (the outer QUIC/mimicry cert reuses the Aura server
|
//! 2. [`MultiServer::bind`] on the `[transport]` endpoints (UDP / TCP / QUIC, per `order`), reusing
|
||||||
//! leaf PEM, as the transport docs suggest).
|
//! the listen IP from `[server] listen`. The QUIC outer (mimicry) cert reuses the Aura server leaf.
|
||||||
//! 3. Start the admin IPC listener over a shared (empty) [`RouteTable`] + [`Stats`].
|
//! 3. Start the admin IPC listener over a shared (empty) [`RouteTable`] + [`Stats`].
|
||||||
//! 4. Accept loop: for each authenticated [`AuraConnection`], create a single shared server-side TUN
|
//! 4. Accept loop: for each [`aura_transport::Accepted`] connection (regardless of which transport
|
||||||
//! on `[tunnel] pool_cidr` (the network's first host address) and run [`AuraRouter`] to bridge
|
//! carried it), create a server-side TUN on `[tunnel] pool_cidr` (the network's first host
|
||||||
//! the connection and the TUN.
|
//! address) and run [`AuraRouter`] to bridge the connection and the TUN.
|
||||||
//!
|
//!
|
||||||
//! ## Privilege / scope notes (NOT auto-tested)
|
//! ## Privilege / scope notes (NOT auto-tested)
|
||||||
//! * Creating the TUN ([`AuraTun::create`]) needs **root** — the accept loop's data path is
|
//! * Creating the TUN ([`AuraTun::create`]) needs **root** — the accept loop's data path is
|
||||||
//! therefore exercised only in a live, privileged run, not in unit tests.
|
//! therefore exercised only in a live, privileged run, not in unit tests.
|
||||||
//! * Binding a UDP socket on `[server] listen` (e.g. `:443`) typically needs privileges too.
|
//! * Binding the transport sockets on the configured ports (e.g. `:443`) typically needs privileges.
|
||||||
//! * Multi-client IP-pool allocation / NAT is **out of v1 scope**: v1 bridges to one shared TUN, so
|
//! * Multi-client IP-pool allocation / NAT is **out of v1 scope**: v1 bridges to one shared TUN, so
|
||||||
//! it is correct for a single active client. The accept loop still accepts many connections (each
|
//! it is correct for a single active client. The accept loop still accepts many connections (each
|
||||||
//! gets its own router task), which is enough to demonstrate the end-to-end path.
|
//! gets its own router task), which is enough to demonstrate the end-to-end path. (The custom-UDP
|
||||||
|
//! backend is single-peer-per-accept in v1; prefer TCP/QUIC for many concurrent clients.)
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use aura_transport::AuraServer;
|
use aura_transport::MultiServer;
|
||||||
use aura_tunnel::{AuraRouter, AuraTun, RouteAction, RouteTable};
|
use aura_tunnel::{AuraRouter, AuraTun, RouteAction, RouteTable};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
@@ -36,6 +38,11 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
|||||||
let proto_cfg = cfg.to_proto()?;
|
let proto_cfg = cfg.to_proto()?;
|
||||||
let pool = cfg.pool_network()?;
|
let pool = cfg.pool_network()?;
|
||||||
|
|
||||||
|
// Per-transport endpoints (UDP/TCP/QUIC) derived from the listen IP + `[transport]` ports.
|
||||||
|
let endpoints = cfg.transport_endpoints()?;
|
||||||
|
let udp_opts = cfg.udp_opts();
|
||||||
|
let tcp_opts = cfg.tcp_opts();
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
name = %cfg.server.name,
|
name = %cfg.server.name,
|
||||||
%listen,
|
%listen,
|
||||||
@@ -44,19 +51,21 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
|||||||
dns = ?cfg.tunnel.dns,
|
dns = ?cfg.tunnel.dns,
|
||||||
mimicry_sni = ?cfg.mimicry.sni,
|
mimicry_sni = ?cfg.mimicry.sni,
|
||||||
mimicry_padding = cfg.mimicry.padding,
|
mimicry_padding = cfg.mimicry.padding,
|
||||||
|
transport_order = ?cfg.transport.order,
|
||||||
|
udp = ?endpoints.udp,
|
||||||
|
tcp = ?endpoints.tcp,
|
||||||
|
quic = ?endpoints.quic,
|
||||||
|
obfuscate = udp_opts.obfuscate,
|
||||||
|
masquerade = tcp_opts.masquerade,
|
||||||
"starting Aura server"
|
"starting Aura server"
|
||||||
);
|
);
|
||||||
|
|
||||||
// The outer (mimicry) QUIC cert reuses the Aura server leaf, matching the transport's guidance.
|
// Bind every enabled transport at once. The QUIC outer (mimicry) cert reuses the Aura server
|
||||||
let server = AuraServer::bind(
|
// leaf inside `proto_cfg`, matching the transport's guidance.
|
||||||
listen,
|
let mut server = MultiServer::bind(endpoints, proto_cfg.clone(), udp_opts, tcp_opts)
|
||||||
&proto_cfg.server_cert_pem,
|
.await
|
||||||
&proto_cfg.server_key_pem,
|
.context("binding Aura multi-transport server")?;
|
||||||
proto_cfg.clone(),
|
tracing::info!("Aura server bound on all enabled transports");
|
||||||
)
|
|
||||||
.context("binding Aura server")?;
|
|
||||||
let bound = server.local_addr().context("reading bound address")?;
|
|
||||||
tracing::info!(%bound, "Aura server bound");
|
|
||||||
|
|
||||||
// Shared routing table (server-side classification is trivial in v1: everything via VPN) +
|
// Shared routing table (server-side classification is trivial in v1: everything via VPN) +
|
||||||
// stats, exposed over the admin socket.
|
// stats, exposed over the admin socket.
|
||||||
@@ -75,13 +84,15 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Accept loop. Each accepted connection gets a server-side TUN and a router task.
|
// Accept loop. Each accepted connection (from any transport) gets a server-side TUN and a router
|
||||||
|
// task. `MultiServer::accept` yields `None` only when every transport's accept loop has stopped.
|
||||||
let mtu = cfg.tunnel.mtu;
|
let mtu = cfg.tunnel.mtu;
|
||||||
loop {
|
while let Some(accepted) = server.accept().await {
|
||||||
let conn = server.accept().await.context("accepting connection")?;
|
let peer = accepted.peer_id.clone();
|
||||||
let peer = conn.peer_id().map(str::to_owned);
|
let mode = accepted.mode;
|
||||||
|
let conn = accepted.conn;
|
||||||
stats.set_peer_id(peer.clone());
|
stats.set_peer_id(peer.clone());
|
||||||
tracing::info!(peer = ?peer, "accepted authenticated client");
|
tracing::info!(peer = ?peer, %mode, "accepted authenticated client");
|
||||||
|
|
||||||
let routes = Arc::clone(&routes);
|
let routes = Arc::clone(&routes);
|
||||||
let tun_ip = first_host(pool);
|
let tun_ip = first_host(pool);
|
||||||
@@ -94,12 +105,15 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let router = AuraRouter::new(tun, routes, conn.into_dyn());
|
let router = AuraRouter::new(tun, routes, conn);
|
||||||
if let Err(e) = router.run().await {
|
if let Err(e) = router.run().await {
|
||||||
tracing::warn!(peer = ?peer, error = %e, "server router stopped");
|
tracing::warn!(peer = ?peer, %mode, error = %e, "server router stopped");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::warn!("all transport accept loops stopped; server exiting");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The first usable host address of a network (network address + 1 for IPv4; the network address
|
/// The first usable host address of a network (network address + 1 for IPv4; the network address
|
||||||
|
|||||||
@@ -1,65 +1,174 @@
|
|||||||
//! CLI-level end-to-end loopback (no TUN): mint certs via [`aura_pki::AuraCa`], build proto
|
//! CLI-level end-to-end loopback (no TUN, no root): build the CLI's `server.toml` / `client.toml`
|
||||||
//! Client/Server configs, [`AuraServer::bind`] on `127.0.0.1:0`, [`AuraClient::connect`], and
|
//! structs from real TOML, derive the transport wiring through the **CLI config helpers**
|
||||||
//! exchange packets via the [`PacketConnection`] API, asserting integrity.
|
//! ([`ServerConfigFile::transport_endpoints`] / [`ClientConfigFile::dial_config`]), bind a real
|
||||||
|
//! [`aura_transport::MultiServer`], [`aura_transport::dial`] it, and exchange packets over the
|
||||||
|
//! returned [`PacketConnection`] — asserting integrity and the negotiated transport mode.
|
||||||
//!
|
//!
|
||||||
//! This is the full CLI integration path short of the privileged TUN device: it proves the crate's
|
//! This proves the CLI builds correct `Endpoints` / `DialConfig` from config and that the new
|
||||||
//! wiring of aura-pki + aura-proto + aura-transport works end to end without root or external
|
//! multi-transport server + dialer connect end to end. It is the full CLI integration path short of
|
||||||
//! network.
|
//! the privileged TUN device (which needs root and is therefore exercised only in a live run).
|
||||||
|
//!
|
||||||
|
//! UDP-only is used so the test can learn a single free loopback port up front (the custom-UDP
|
||||||
|
//! backend is single-peer-per-accept in v1, which is exactly one client here). The fallback/handover
|
||||||
|
//! logic itself is unit-tested in `aura-transport`; here we prove the CLI feeds it correct configs.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use aura_cli::config::{ClientConfigFile, ServerConfigFile};
|
||||||
use aura_pki::AuraCa;
|
use aura_pki::AuraCa;
|
||||||
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
|
use aura_proto::PacketConnection;
|
||||||
use aura_transport::{AuraClient, AuraServer};
|
use aura_transport::{dial, MultiServer, TransportMode};
|
||||||
|
|
||||||
const SERVER_NAME: &str = "localhost";
|
const SERVER_NAME: &str = "localhost";
|
||||||
const CAMOUFLAGE_SNI: &str = "cdn.example.com";
|
|
||||||
|
/// A unique temp directory for this test process (no `tempfile` dependency in the workspace).
|
||||||
|
fn temp_dir(tag: &str) -> PathBuf {
|
||||||
|
let mut dir = std::env::temp_dir();
|
||||||
|
dir.push(format!(
|
||||||
|
"aura-cli-test-{tag}-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&dir).expect("create temp dir");
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grab a currently-free UDP port on loopback by binding `:0` and immediately releasing it. On the
|
||||||
|
/// loopback interface in a test process the window before we rebind it is negligible and
|
||||||
|
/// deterministic enough for CI.
|
||||||
|
fn free_udp_port() -> u16 {
|
||||||
|
let sock = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind ephemeral udp");
|
||||||
|
sock.local_addr().expect("local_addr").port()
|
||||||
|
// `sock` drops here, freeing the port for MultiServer to re-bind.
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn cli_loopback_packet_exchange() {
|
async fn cli_config_drives_multiserver_and_dial() {
|
||||||
// PKI: CA + server cert (SAN localhost) + client cert.
|
let dir = temp_dir("loopback");
|
||||||
|
|
||||||
|
// PKI: CA + server cert (SAN localhost) + client cert, written to PEM files the CLI config reads.
|
||||||
let ca = AuraCa::generate("Aura CLI Test CA").expect("generate CA");
|
let ca = AuraCa::generate("Aura CLI Test CA").expect("generate CA");
|
||||||
let server_cert = ca.issue_server_cert(SERVER_NAME).expect("server cert");
|
let server_cert = ca.issue_server_cert(SERVER_NAME).expect("server cert");
|
||||||
let client_cert = ca.issue_client_cert("cli-client").expect("client cert");
|
let client_cert = ca.issue_client_cert("cli-client").expect("client cert");
|
||||||
let ca_pem = ca.ca_cert_pem();
|
|
||||||
|
|
||||||
let server_cfg = ServerConfig {
|
let ca_path = dir.join("ca.crt");
|
||||||
ca_cert_pem: ca_pem.clone(),
|
let srv_cert_path = dir.join("server.crt");
|
||||||
server_cert_pem: server_cert.cert_pem.clone(),
|
let srv_key_path = dir.join("server.key");
|
||||||
server_key_pem: server_cert.key_pem.clone(),
|
let cli_cert_path = dir.join("client.crt");
|
||||||
};
|
let cli_key_path = dir.join("client.key");
|
||||||
let client_cfg = ClientConfig {
|
std::fs::write(&ca_path, ca.ca_cert_pem()).unwrap();
|
||||||
ca_cert_pem: ca_pem.clone(),
|
std::fs::write(&srv_cert_path, &server_cert.cert_pem).unwrap();
|
||||||
client_cert_pem: client_cert.cert_pem.clone(),
|
std::fs::write(&srv_key_path, &server_cert.key_pem).unwrap();
|
||||||
client_key_pem: client_cert.key_pem.clone(),
|
std::fs::write(&cli_cert_path, &client_cert.cert_pem).unwrap();
|
||||||
server_name: SERVER_NAME.to_string(),
|
std::fs::write(&cli_key_path, &client_cert.key_pem).unwrap();
|
||||||
};
|
|
||||||
|
|
||||||
// Bind on an OS-assigned loopback port.
|
// UDP-only on a learned free loopback port. SNI must match the server cert SAN (used as the inner
|
||||||
let server = AuraServer::bind(
|
// handshake server_name) so mutual auth succeeds.
|
||||||
"127.0.0.1:0".parse().unwrap(),
|
let udp_port = free_udp_port();
|
||||||
&server_cert.cert_pem,
|
|
||||||
&server_cert.key_pem,
|
|
||||||
server_cfg,
|
|
||||||
)
|
|
||||||
.expect("bind server");
|
|
||||||
let server_addr = server.local_addr().expect("local_addr");
|
|
||||||
|
|
||||||
// Accept + connect concurrently.
|
let server_toml = format!(
|
||||||
let accept = tokio::spawn(async move { server.accept().await });
|
r#"
|
||||||
let connect =
|
[server]
|
||||||
tokio::spawn(
|
name = "edge-test"
|
||||||
async move { AuraClient::connect(server_addr, CAMOUFLAGE_SNI, client_cfg).await },
|
listen = "127.0.0.1:{udp_port}"
|
||||||
|
|
||||||
|
[pki]
|
||||||
|
ca_cert = "{ca}"
|
||||||
|
cert = "{cert}"
|
||||||
|
key = "{key}"
|
||||||
|
|
||||||
|
[tunnel]
|
||||||
|
pool_cidr = "10.7.0.0/24"
|
||||||
|
|
||||||
|
[transport]
|
||||||
|
order = ["udp"]
|
||||||
|
udp_port = {udp_port}
|
||||||
|
quic_port = {quic_port}
|
||||||
|
obfuscate = false
|
||||||
|
"#,
|
||||||
|
ca = ca_path.display(),
|
||||||
|
cert = srv_cert_path.display(),
|
||||||
|
key = srv_key_path.display(),
|
||||||
|
quic_port = udp_port + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
let server_conn = accept.await.expect("accept join").expect("accept");
|
let client_toml = format!(
|
||||||
let client_conn = connect.await.expect("connect join").expect("connect");
|
r#"
|
||||||
|
[client]
|
||||||
|
name = "laptop-test"
|
||||||
|
server_addr = "127.0.0.1:{udp_port}"
|
||||||
|
sni = "{sni}"
|
||||||
|
|
||||||
// Mutual auth established the client's verified CN on the server side.
|
[pki]
|
||||||
assert_eq!(server_conn.peer_id(), Some("cli-client"));
|
ca_cert = "{ca}"
|
||||||
|
cert = "{cert}"
|
||||||
|
key = "{key}"
|
||||||
|
|
||||||
let server_conn: Arc<dyn PacketConnection> = Arc::new(server_conn);
|
[tunnel]
|
||||||
let client_conn: Arc<dyn PacketConnection> = Arc::new(client_conn);
|
local_ip = "10.7.0.2"
|
||||||
|
|
||||||
|
[transport]
|
||||||
|
order = ["udp"]
|
||||||
|
udp_port = {udp_port}
|
||||||
|
quic_port = {quic_port}
|
||||||
|
obfuscate = false
|
||||||
|
"#,
|
||||||
|
sni = SERVER_NAME,
|
||||||
|
ca = ca_path.display(),
|
||||||
|
cert = cli_cert_path.display(),
|
||||||
|
key = cli_key_path.display(),
|
||||||
|
quic_port = udp_port + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let server_cfg = ServerConfigFile::parse(&server_toml).expect("parse server.toml");
|
||||||
|
let client_cfg = ClientConfigFile::parse(&client_toml).expect("parse client.toml");
|
||||||
|
|
||||||
|
// Derive the transport wiring through the actual CLI helpers (the thing under test).
|
||||||
|
let endpoints = server_cfg.transport_endpoints().expect("server endpoints");
|
||||||
|
assert_eq!(
|
||||||
|
endpoints.udp.unwrap().to_string(),
|
||||||
|
format!("127.0.0.1:{udp_port}")
|
||||||
|
);
|
||||||
|
assert!(endpoints.tcp.is_none() && endpoints.quic.is_none());
|
||||||
|
|
||||||
|
let server_proto = server_cfg.to_proto().expect("server proto cfg");
|
||||||
|
let client_proto = client_cfg.to_proto().expect("client proto cfg");
|
||||||
|
let dial_cfg = client_cfg.dial_config().expect("client dial config");
|
||||||
|
assert_eq!(dial_cfg.order, vec![TransportMode::Udp]);
|
||||||
|
|
||||||
|
// Bind every enabled transport (just UDP here) via the new MultiServer.
|
||||||
|
let mut server = MultiServer::bind(
|
||||||
|
endpoints,
|
||||||
|
server_proto,
|
||||||
|
server_cfg.udp_opts(),
|
||||||
|
server_cfg.tcp_opts(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("bind MultiServer");
|
||||||
|
|
||||||
|
// Accept + dial concurrently.
|
||||||
|
let accept = tokio::spawn(async move { server.accept().await.map(|a| (a, server)) });
|
||||||
|
let connect = tokio::spawn(async move { dial(client_proto, dial_cfg).await });
|
||||||
|
|
||||||
|
let (accepted, _server_keepalive) = accept
|
||||||
|
.await
|
||||||
|
.expect("accept join")
|
||||||
|
.expect("MultiServer accepted a connection");
|
||||||
|
let (client_conn, mode): (Arc<dyn PacketConnection>, TransportMode) = connect
|
||||||
|
.await
|
||||||
|
.expect("connect join")
|
||||||
|
.expect("dial connected");
|
||||||
|
|
||||||
|
// The handover picked UDP, and the server verified the client's CN via mutual auth.
|
||||||
|
assert_eq!(mode, TransportMode::Udp);
|
||||||
|
assert_eq!(accepted.mode, TransportMode::Udp);
|
||||||
|
assert_eq!(accepted.peer_id.as_deref(), Some("cli-client"));
|
||||||
|
|
||||||
|
let server_conn = accepted.conn;
|
||||||
|
|
||||||
// Client -> server.
|
// Client -> server.
|
||||||
for pkt in [
|
for pkt in [
|
||||||
@@ -78,4 +187,6 @@ async fn cli_loopback_packet_exchange() {
|
|||||||
let got = client_conn.recv_packet().await.expect("client recv");
|
let got = client_conn.recv_packet().await.expect("client recv");
|
||||||
assert_eq!(got, pkt);
|
assert_eq!(got, pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,12 @@ impl AeadKey {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns [`CryptoError::AeadDecrypt`] if authentication fails (tampered ciphertext, wrong
|
/// Returns [`CryptoError::AeadDecrypt`] if authentication fails (tampered ciphertext, wrong
|
||||||
/// AAD, wrong key, or wrong counter).
|
/// AAD, wrong key, or wrong counter).
|
||||||
pub fn open(&self, counter: u64, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
pub fn open(
|
||||||
|
&self,
|
||||||
|
counter: u64,
|
||||||
|
ciphertext: &[u8],
|
||||||
|
aad: &[u8],
|
||||||
|
) -> Result<Vec<u8>, CryptoError> {
|
||||||
let nonce = AeadSession::nonce_for(counter);
|
let nonce = AeadSession::nonce_for(counter);
|
||||||
self.cipher()
|
self.cipher()
|
||||||
.decrypt(
|
.decrypt(
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ pub mod session;
|
|||||||
pub use conn::PacketConnection;
|
pub use conn::PacketConnection;
|
||||||
pub use frame::{Frame, MsgType};
|
pub use frame::{Frame, MsgType};
|
||||||
pub use handshake::{client_handshake, server_handshake};
|
pub use handshake::{client_handshake, server_handshake};
|
||||||
pub use session::{
|
pub use session::{DatagramReceiver, DatagramSender, Session, SessionReceiver, SessionSender};
|
||||||
DatagramReceiver, DatagramSender, Session, SessionReceiver, SessionSender,
|
|
||||||
};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|||||||
@@ -404,7 +404,9 @@ impl DatagramReceiver {
|
|||||||
/// * [`ProtoError::MalformedFrame`] — datagram too short or undecodable frame.
|
/// * [`ProtoError::MalformedFrame`] — datagram too short or undecodable frame.
|
||||||
pub fn open(&mut self, datagram: &[u8]) -> Result<Frame, ProtoError> {
|
pub fn open(&mut self, datagram: &[u8]) -> Result<Frame, ProtoError> {
|
||||||
if datagram.len() < SEQ_LEN {
|
if datagram.len() < SEQ_LEN {
|
||||||
return Err(ProtoError::MalformedFrame("datagram shorter than seq prefix"));
|
return Err(ProtoError::MalformedFrame(
|
||||||
|
"datagram shorter than seq prefix",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut seq_be = [0u8; SEQ_LEN];
|
let mut seq_be = [0u8; SEQ_LEN];
|
||||||
seq_be.copy_from_slice(&datagram[..SEQ_LEN]);
|
seq_be.copy_from_slice(&datagram[..SEQ_LEN]);
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ impl FromStr for TransportMode {
|
|||||||
"udp" => Ok(Self::Udp),
|
"udp" => Ok(Self::Udp),
|
||||||
"tcp" => Ok(Self::Tcp),
|
"tcp" => Ok(Self::Tcp),
|
||||||
"quic" => Ok(Self::Quic),
|
"quic" => Ok(Self::Quic),
|
||||||
other => Err(format!("unknown transport '{other}' (expected udp|tcp|quic)")),
|
other => Err(format!(
|
||||||
|
"unknown transport '{other}' (expected udp|tcp|quic)"
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +119,8 @@ pub async fn dial(
|
|||||||
};
|
};
|
||||||
tracing::info!("dial: trying {mode} at {addr}");
|
tracing::info!("dial: trying {mode} at {addr}");
|
||||||
let attempt =
|
let attempt =
|
||||||
tokio::time::timeout(cfg.attempt_timeout, dial_one(*mode, addr, &proto_cfg, &cfg)).await;
|
tokio::time::timeout(cfg.attempt_timeout, dial_one(*mode, addr, &proto_cfg, &cfg))
|
||||||
|
.await;
|
||||||
match attempt {
|
match attempt {
|
||||||
Ok(Ok(conn)) => {
|
Ok(Ok(conn)) => {
|
||||||
tracing::info!("dial: connected via {mode}");
|
tracing::info!("dial: connected via {mode}");
|
||||||
|
|||||||
@@ -109,7 +109,11 @@ impl PacketConnection for TcpConnection {
|
|||||||
Frame::Data { payload, .. } => return Ok(payload.to_vec()),
|
Frame::Data { payload, .. } => return Ok(payload.to_vec()),
|
||||||
Frame::Ping { seq } => {
|
Frame::Ping { seq } => {
|
||||||
// Separate mutex from the receive lock we hold => no deadlock.
|
// Separate mutex from the receive lock we hold => no deadlock.
|
||||||
self.sender.lock().await.send_frame(Frame::Pong { seq }).await?;
|
self.sender
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.send_frame(Frame::Pong { seq })
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Frame::Pong { .. } => continue,
|
Frame::Pong { .. } => continue,
|
||||||
Frame::Close { code, reason } => {
|
Frame::Close { code, reason } => {
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ use aura_transport::{dial, DialConfig, Endpoints, TcpOpts, TransportMode, UdpOpt
|
|||||||
|
|
||||||
fn make_configs() -> (ServerConfig, ClientConfig) {
|
fn make_configs() -> (ServerConfig, ClientConfig) {
|
||||||
let ca = AuraCa::generate("Aura Test CA").expect("generate CA");
|
let ca = AuraCa::generate("Aura Test CA").expect("generate CA");
|
||||||
let server = ca.issue_server_cert("localhost").expect("issue server cert");
|
let server = ca
|
||||||
let client = ca.issue_client_cert("client-dial").expect("issue client cert");
|
.issue_server_cert("localhost")
|
||||||
|
.expect("issue server cert");
|
||||||
|
let client = ca
|
||||||
|
.issue_client_cert("client-dial")
|
||||||
|
.expect("issue client cert");
|
||||||
let ca_pem = ca.ca_cert_pem();
|
let ca_pem = ca.ca_cert_pem();
|
||||||
(
|
(
|
||||||
ServerConfig {
|
ServerConfig {
|
||||||
@@ -33,8 +37,8 @@ async fn dial_falls_back_from_dead_tcp_to_udp() {
|
|||||||
let (scfg, ccfg) = make_configs();
|
let (scfg, ccfg) = make_configs();
|
||||||
|
|
||||||
// A real UDP server (the working fallback target).
|
// A real UDP server (the working fallback target).
|
||||||
let udp_server =
|
let udp_server = UdpServer::bind("127.0.0.1:0".parse().unwrap(), scfg, UdpOpts::default())
|
||||||
UdpServer::bind("127.0.0.1:0".parse().unwrap(), scfg, UdpOpts::default()).expect("bind udp");
|
.expect("bind udp");
|
||||||
let udp_addr = udp_server.local_addr().expect("udp addr");
|
let udp_addr = udp_server.local_addr().expect("udp addr");
|
||||||
let srv = tokio::spawn(async move {
|
let srv = tokio::spawn(async move {
|
||||||
let conn = udp_server.accept().await.expect("server accept");
|
let conn = udp_server.accept().await.expect("server accept");
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ use aura_transport::{TcpClient, TcpOpts, TcpServer};
|
|||||||
/// Mint a fresh CA + server("localhost") + client("client-tcp") and build the proto configs.
|
/// Mint a fresh CA + server("localhost") + client("client-tcp") and build the proto configs.
|
||||||
fn make_configs() -> (ServerConfig, ClientConfig) {
|
fn make_configs() -> (ServerConfig, ClientConfig) {
|
||||||
let ca = AuraCa::generate("Aura Test CA").expect("generate CA");
|
let ca = AuraCa::generate("Aura Test CA").expect("generate CA");
|
||||||
let server = ca.issue_server_cert("localhost").expect("issue server cert");
|
let server = ca
|
||||||
let client = ca.issue_client_cert("client-tcp").expect("issue client cert");
|
.issue_server_cert("localhost")
|
||||||
|
.expect("issue server cert");
|
||||||
|
let client = ca
|
||||||
|
.issue_client_cert("client-tcp")
|
||||||
|
.expect("issue client cert");
|
||||||
let ca_pem = ca.ca_cert_pem();
|
let ca_pem = ca.ca_cert_pem();
|
||||||
let scfg = ServerConfig {
|
let scfg = ServerConfig {
|
||||||
ca_cert_pem: ca_pem.clone(),
|
ca_cert_pem: ca_pem.clone(),
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Integrating AuraVPN with sing-box (approach note)
|
||||||
|
|
||||||
|
Goal: let a phone client running **sing-box** connect to an Aura server speaking the **AuraVPN**
|
||||||
|
protocol (Aura's own tunneling — not a third party's). This is a short note on *how*; the wire
|
||||||
|
protocol it must match is fully specified in [`protocol.md`](protocol.md).
|
||||||
|
|
||||||
|
sing-box is written in Go and has no generic "load an arbitrary external wire protocol" plugin, so
|
||||||
|
integration means giving sing-box a Go implementation (or a bridge) of the Aura protocol. Three
|
||||||
|
realistic paths, cheapest first:
|
||||||
|
|
||||||
|
## Option A — Process bridge (fastest, desktop/server)
|
||||||
|
|
||||||
|
Run the existing Rust `aura` client as a local process and expose a local proxy/TUN, then point
|
||||||
|
sing-box at it:
|
||||||
|
|
||||||
|
- `aura client` already creates a TUN and routes through the Aura tunnel; sing-box can route selected
|
||||||
|
traffic into that interface, **or**
|
||||||
|
- add a local **SOCKS5 inbound** to `aura-cli` (small addition) and configure a sing-box `socks`
|
||||||
|
outbound pointing at `127.0.0.1:<port>`.
|
||||||
|
|
||||||
|
Pros: reuses the audited Rust core verbatim; no crypto re-implementation. Cons: two processes; weak
|
||||||
|
fit for mobile (sing-box mobile apps embed the core and don't spawn helpers easily).
|
||||||
|
|
||||||
|
## Option B — Native Go outbound/inbound (the real target for phones)
|
||||||
|
|
||||||
|
Implement the AuraVPN protocol natively in Go and register it as a sing-box **outbound** (client) and
|
||||||
|
**inbound** (server), so the phone's embedded sing-box core speaks AuraVPN directly. This is the
|
||||||
|
clean, performant, mobile-friendly path. Crypto maps cleanly to existing Go libraries:
|
||||||
|
|
||||||
|
| Aura piece | Rust crate | Go equivalent |
|
||||||
|
|---|---|---|
|
||||||
|
| X25519 ECDH | `x25519-dalek` | `crypto/ecdh` (stdlib) |
|
||||||
|
| ML-KEM-768 (FIPS 203) | `ml-kem` | `crypto/mlkem` (Go 1.24+) or `cloudflare/circl` |
|
||||||
|
| ChaCha20-Poly1305 | `chacha20poly1305` | `golang.org/x/crypto/chacha20poly1305` |
|
||||||
|
| HKDF-SHA256 | `hkdf` | `golang.org/x/crypto/hkdf` |
|
||||||
|
| HMAC-SHA256 (Finished) | `hmac` | `crypto/hmac` + `crypto/sha256` |
|
||||||
|
| ECDSA P-256 sigs (cert auth) | `ring` | `crypto/ecdsa` + `crypto/x509` |
|
||||||
|
| X.509 verify + CRL | `rustls-webpki` | `crypto/x509` |
|
||||||
|
|
||||||
|
What the Go code must reproduce **exactly** (see `protocol.md`):
|
||||||
|
- 5-byte frame header `msg_type(1) || len(u24 BE) || version=0x01`.
|
||||||
|
- Handshake order CH → SH → ServerAuth → ClientAuth → Finished(c→s) → Finished(s→c); transcript =
|
||||||
|
`SHA-256(ClientHello_frame || ServerHello_frame)`; ECDSA-P256/SHA-256 signature over the transcript;
|
||||||
|
HMAC-SHA256 Finished.
|
||||||
|
- Hybrid shared secret = `x25519_ss || mlkem_ss`; HKDF salt = `client_nonce || server_nonce`,
|
||||||
|
info = `b"aura-v1-session"`.
|
||||||
|
- Data record (datagram/UDP) = `seq(8 BE) || ChaCha20Poly1305(frame, aad = seq)`, nonce =
|
||||||
|
`LE(seq) || 0x00000000`; replay window 64. (Stream/TCP record adds the 5-byte header to the AAD.)
|
||||||
|
- Transport selection: UDP (type `0x01` HS / `0x02` DATA) primary; TCP/443 and QUIC fallbacks.
|
||||||
|
|
||||||
|
To de-risk the Go port, export **known-answer test vectors** from the Rust side (a captured
|
||||||
|
handshake transcript + derived keys + a sealed data record) and assert the Go implementation
|
||||||
|
reproduces them byte-for-byte. The ML-KEM KAT already lives in `aura-crypto/tests/kat_kyber.rs`.
|
||||||
|
|
||||||
|
## Option C — Rust core via cgo (`cdylib`)
|
||||||
|
|
||||||
|
Compile the Aura Rust core to a C-ABI shared library and call it from a thin sing-box Go shim via
|
||||||
|
cgo. Reuses the audited crypto/handshake with no Go re-implementation, but cgo + per-platform
|
||||||
|
(Android/iOS) packaging is fiddly and complicates sing-box's pure-Go build.
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
- **Now:** Option A (process bridge) for desktop/server validation — minimal work, real protocol.
|
||||||
|
- **For the phone:** Option B (native Go outbound), built against `protocol.md` + exported Rust test
|
||||||
|
vectors. It is the only option that fits sing-box's embedded mobile core well.
|
||||||
|
- Keep `protocol.md` the single source of truth and version the wire protocol (the header already
|
||||||
|
carries `version = 0x01`) so the Rust and Go implementations stay in lockstep.
|
||||||
|
|
||||||
|
> Status: this is a design note. No Go code or sing-box module is implemented yet — that is a
|
||||||
|
> separate deliverable tracked for after the Rust transport stabilizes.
|
||||||
Reference in New Issue
Block a user