feat(cli,tunnel,docs): full Windows support — OS routes + wintun audit
Windows is now first-class for client use: - aura-cli::os_routes Windows path is no longer a stub. Real install via `route ADD <net> MASK <mask> <gw> METRIC 1` for DIRECT bypass (rollback: `route DELETE ...`) and `netsh interface ipv4 add route <cidr> "Aura" <tun_local_ip> store=active` for VPN default/CIDR (rollback: `netsh ... delete route ...`). Default-gateway detection by parsing `route print 0` output via parse_windows_route_print_default; rejects `On-link` rows. Dry run works on every host. - aura-tunnel::tun wintun audit fixed a real bug: AuraTun was holding only Arc<Session> while Session does NOT keep Arc<Adapter> alive (only the Wintun DLL handle). On Drop the adapter was being closed under the session. Fixed by adding _adapter: Arc<wintun::Adapter> to AuraTun, with field order ensuring Session is dropped before Adapter so end-session precedes close-adapter. Also wired mtu into write_packet (hard limit) + read_packet (warn). - Cross-compile verified: cargo check --target x86_64-pc-windows-gnu --workspace and clippy on the windows target are both clean (added mingw-w64 + x86_64-pc-windows-gnu via rustup). - docs/deployment.md: §6 updated (Windows OS-routes now Done), new §8 «Windows как клиент» with download wintun.dll, Admin run, [tunnel.os_routes] enabled, known no-ops (run_as, [server.nat]). 9 new tests (7 parser/plan/undo unit + 1 windows dry-run integration + 1 existing). Workspace: 293 tests passed (+9), clippy -D warnings clean, fmt clean. macOS host + windows-gnu cross-target both green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -37,8 +37,16 @@ pub struct AuraTun {
|
||||
#[cfg(not(windows))]
|
||||
mtu: u16,
|
||||
|
||||
/// Active wintun session. `Session::Drop` ends the session via `WintunEndSession`.
|
||||
#[cfg(windows)]
|
||||
inner: std::sync::Arc<wintun::Session>,
|
||||
/// Keep the wintun adapter alive for the lifetime of the session. `wintun::Session` only
|
||||
/// holds an `Arc<Wintun>` (the DLL handle), NOT an `Arc<Adapter>` — if the adapter is
|
||||
/// dropped, its `WintunCloseAdapter` runs and the session's underlying handle is
|
||||
/// invalidated. Holding the `Arc<Adapter>` here is what guarantees the adapter outlives
|
||||
/// the session.
|
||||
#[cfg(windows)]
|
||||
_adapter: std::sync::Arc<wintun::Adapter>,
|
||||
#[cfg(windows)]
|
||||
mtu: u16,
|
||||
}
|
||||
@@ -141,8 +149,14 @@ impl AuraTun {
|
||||
IpAddr::V6(_) => unreachable!("v4 address yields a v4 mask"),
|
||||
};
|
||||
|
||||
// SAFETY: loads the bundled wintun.dll via its documented entry point.
|
||||
// SAFETY: loads the bundled wintun.dll (expected next to aura.exe). The wintun crate
|
||||
// documents this `load()` call as the entry point for in-process driver loading; failure
|
||||
// here usually means wintun.dll is not on the PATH / app directory.
|
||||
let wintun = unsafe { wintun::load() }.context("failed to load wintun.dll")?;
|
||||
// Adapter name is the display name used by Windows (also what `netsh ... "Aura"`
|
||||
// references in [`crate::os_routes::windows_apply_plan`]). "Aura" doubles as the
|
||||
// tunnel-type string — wintun groups adapters by tunnel_type, so all aura sessions
|
||||
// appear under one category in Device Manager.
|
||||
let adapter = wintun::Adapter::create(&wintun, name, "Aura", None)
|
||||
.with_context(|| format!("failed to create wintun adapter '{name}'"))?;
|
||||
adapter
|
||||
@@ -156,26 +170,58 @@ impl AuraTun {
|
||||
.start_session(wintun::MAX_RING_CAPACITY)
|
||||
.context("failed to start wintun session")?;
|
||||
|
||||
// Hold both the Arc<Adapter> and the Session: Session::Drop calls WintunEndSession, then
|
||||
// Adapter::Drop calls WintunCloseAdapter — that ordering matches what the wintun crate
|
||||
// docs prescribe (end the session before closing the adapter handle). Struct fields are
|
||||
// dropped in declaration order, so `inner` (Session) drops first, then `_adapter`.
|
||||
Ok(Self {
|
||||
inner: std::sync::Arc::new(session),
|
||||
_adapter: adapter,
|
||||
mtu,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read one IP packet from the wintun session.
|
||||
///
|
||||
/// `receive_blocking` is a blocking call, so it runs on a blocking thread to avoid stalling the
|
||||
/// async runtime.
|
||||
/// `receive_blocking` is a blocking call (it parks on the wintun ring's read event), so it
|
||||
/// runs on a blocking thread to avoid stalling the tokio runtime. The returned `Packet` owns
|
||||
/// a slice into the ring buffer; we copy it out to a `Vec` because the ring slot is freed on
|
||||
/// `Packet::Drop` (the next read overwrites it). MTU is checked only as a sanity bound — the
|
||||
/// wintun ring itself is fixed at 64 KiB, but receiving anything larger than the negotiated
|
||||
/// MTU means the OS is doing something wrong upstream.
|
||||
#[cfg(windows)]
|
||||
pub async fn read_packet(&mut self) -> anyhow::Result<Vec<u8>> {
|
||||
let session = self.inner.clone();
|
||||
let packet = tokio::task::spawn_blocking(move || session.receive_blocking()).await??;
|
||||
Ok(packet.bytes().to_vec())
|
||||
let bytes = packet.bytes();
|
||||
if bytes.len() > self.mtu as usize {
|
||||
tracing::warn!(
|
||||
target: "aura::tun",
|
||||
len = bytes.len(),
|
||||
mtu = self.mtu,
|
||||
"wintun packet larger than configured MTU; forwarding anyway"
|
||||
);
|
||||
}
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
/// Write one IP packet to the wintun session.
|
||||
///
|
||||
/// `allocate_send_packet` reserves a slot in the send ring; we fill it with `bytes_mut()`
|
||||
/// then `send_packet` hands the slot back to the driver for transmission. The size cast to
|
||||
/// `u16` is the wintun-imposed per-packet limit (the API takes `u16`, mirroring an
|
||||
/// ETHERNET-class frame). Packets larger than [`Self::mtu`] are rejected up front so the
|
||||
/// allocation does not even happen — that matches the Unix `tun` crate's behaviour where
|
||||
/// `write` rejects oversized frames at the syscall layer.
|
||||
#[cfg(windows)]
|
||||
pub async fn write_packet(&mut self, packet: &[u8]) -> anyhow::Result<()> {
|
||||
if packet.len() > self.mtu as usize {
|
||||
anyhow::bail!(
|
||||
"outbound packet ({} bytes) exceeds wintun MTU ({})",
|
||||
packet.len(),
|
||||
self.mtu
|
||||
);
|
||||
}
|
||||
let len: u16 = packet
|
||||
.len()
|
||||
.try_into()
|
||||
|
||||
Reference in New Issue
Block a user