diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..1a4cdfd --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,19 @@ +# Rusnel cargo config. +# +# Applied automatically by cargo to every build run from this +# repository (the file is found via the standard "walk up from CWD" +# search). Keeping it here rather than in a CI script means local +# `cargo build`s on Windows produce the same artifact CI does. + +# Statically link the Microsoft C runtime on `*-windows-msvc` targets. +# Without this the released `.exe` depends on `vcruntime140.dll` and +# friends from the Visual C++ Redistributable, which is not present +# on a stock Windows install — users would hit a "missing DLL" +# popup. With `+crt-static` the binary is genuinely standalone and +# matches the "single static binary" promise we make for Linux and +# macOS. The cost is ~2 MB of extra binary size. +# +# Scoped to `target_env = "msvc"` so MSYS2 / GNU Windows targets +# (`*-windows-gnu`) and every non-Windows target are unaffected. +[target.'cfg(target_env = "msvc")'] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a3c99d..496273f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,3 +33,20 @@ jobs: - name: Test run: cargo test --all + + # Windows-host build/test. The admin API and `rusnel ctl` are + # unix-only (#[cfg(unix)]); the rest of rusnel — tunnels, TLS, + # embedded credentials — must keep compiling and passing tests on + # Windows so the cross-platform claim doesn't silently regress. + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --all + + - name: Test (lib + bin only; integration tests gated to unix) + run: cargo test --lib --bins diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95f195b..3992dc7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,11 +60,8 @@ jobs: os: macos-latest - target: aarch64-apple-darwin os: macos-latest - # Windows is not yet supported: src/server/admin.rs uses - # tokio::net::UnixListener + POSIX `Permissions::set_mode` - # unconditionally. Add `x86_64-pc-windows-msvc` back here - # once the admin API is `#[cfg(unix)]`-gated (or backed by a - # named pipe on Windows). + - target: x86_64-pc-windows-msvc + os: windows-latest steps: - uses: actions/checkout@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5973c01..9b3c3ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ All notable changes to this project are documented in this file. The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.11.0] - 2026-05-06 + +Windows support. Tunnels, TLS, and embedded credentials now compile +and run on `x86_64-pc-windows-msvc`; only the unix-socket-based +admin API and `rusnel ctl` are gated off. + +### Added + +- **Windows build**, gated through CI: a new `windows-latest` job in + `.github/workflows/ci.yml` runs `cargo build --all` and the `--lib + --bins` test set on every PR, and `x86_64-pc-windows-msvc` is back + in `release.yml`'s prebuilt-binary matrix. +- **Static CRT linkage on `*-windows-msvc`** via + `.cargo/config.toml` (`-C target-feature=+crt-static`). The + released `.exe` no longer depends on the Visual C++ Redistributable + (`vcruntime140.dll` etc.) — it runs on a stock Windows install + with no separate runtime install, matching the single-static-binary + promise on Linux and macOS. Adds ~2 MB to the binary. + +### Changed + +- `crate::ctl` and `crate::server::admin` are now `#[cfg(unix)]`. The + `Mode::Ctl` subcommand and its `CtlAction` variants are also + Windows-omitted, so `rusnel --help` shows the platform-correct + command list. +- `--admin-socket` / `--no-admin-socket` are still accepted on + Windows for argument compatibility, but `--admin-socket ` + prints a one-line warning and is ignored. Future named-pipe + support can re-enable this without breaking flags. +- Integration tests that hit the admin API (`tests/admin.rs`) are + gated to unix via `#![cfg(unix)]`. + ## [0.10.1] - 2026-05-05 Release-pipeline fixes shaken out by the first `v0.10.0` tag. diff --git a/Cargo.lock b/Cargo.lock index 2b3c3e3..b1ff8eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "rusnel" -version = "0.10.1" +version = "0.11.0" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index de6c954..f9eea33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rusnel" description = "Rusnel is a fast TCP/UDP tunnel, transported over and encrypted using QUIC protocol. Single executable including both client and server" -version = "0.10.1" +version = "0.11.0" edition = "2021" license = "Apache-2.0" repository = "https://github.com/guyte149/Rusnel" diff --git a/README.md b/README.md index c4c845a..1fe3a4d 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,13 @@ cd Rusnel cargo build --release ``` -Pre-built binaries for Linux (x86_64 + aarch64, gnu and musl) and -macOS (x86_64 + Apple Silicon) are attached to each +Pre-built binaries for Linux (x86_64 + aarch64, gnu and musl), macOS +(x86_64 + Apple Silicon), and Windows (x86_64) are attached to each [GitHub release](https://github.com/guyte149/Rusnel/releases). -Windows is not yet supported (the admin API is Unix-socket-only). + +On Windows the admin HTTP API and the `rusnel ctl` subcommand are +not available — both are Unix-socket-based. Tunnels, TLS, and +embedded credentials work the same as on Unix. ### Docker diff --git a/src/lib.rs b/src/lib.rs index f79ad3b..b9c0348 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,11 @@ use tracing::{debug, error}; pub mod cert; pub mod client; pub mod common; +// `rusnel ctl` and the admin API it speaks to are unix-socket-based. +// Both are gated to unix until a Windows named-pipe backend exists, +// so the rest of rusnel (the data-plane tunnels, TLS, embedded +// credentials) compiles and runs on Windows. +#[cfg(unix)] pub mod ctl; pub mod embedded; pub mod server; diff --git a/src/main.rs b/src/main.rs index 103eb74..f0eb324 100644 --- a/src/main.rs +++ b/src/main.rs @@ -465,6 +465,10 @@ sharing : from the client to the server\'s .sock (macOS / no XDG); pass --socket to /// override. Output defaults to a tab-aligned table; pass --json to /// pipe the raw API response. + /// + /// Unix-only: the admin API speaks HTTP over a unix domain socket. + /// On Windows the subcommand is not compiled in. + #[cfg(unix)] Ctl { /// Path to the admin unix socket. #[arg(long, value_name = "PATH")] @@ -477,6 +481,7 @@ sharing : from the client to the server\'s `. Clap enforces the // mutual-exclusion via `conflicts_with`. + // + // Unix-only: the admin API speaks HTTP over a unix + // domain socket. On Windows we always disable it, + // and warn loudly if the operator explicitly asked + // for one — silently dropping the flag would be + // worse than a one-line warning at startup. + #[cfg(unix)] admin_socket: if no_admin_socket { None } else { Some(admin_socket.unwrap_or_else(rusnel::ctl::default_socket_path)) }, + #[cfg(not(unix))] + admin_socket: { + if admin_socket.is_some() { + eprintln!( + "warning: --admin-socket is unix-only and will be ignored on this platform" + ); + } + let _ = no_admin_socket; + None + }, }; debug!(?server_config, "server config resolved"); run_server(server_config); @@ -1347,6 +1369,7 @@ fn main() { debug!(?client_config, "client config resolved"); run_client(client_config); } + #[cfg(unix)] Mode::Ctl { socket, json, @@ -1374,6 +1397,7 @@ fn main() { } } +#[cfg(unix)] fn run_ctl(socket: &std::path::Path, json: bool, action: CtlAction) -> anyhow::Result<()> { use rusnel::ctl::{self, Format}; let format = if json { Format::Json } else { Format::Table }; diff --git a/src/server/mod.rs b/src/server/mod.rs index b316d8a..fb5c63a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,6 +1,11 @@ +// The admin HTTP API is unix-socket-only (see `crate::ctl`); on +// Windows the module isn't compiled and the server simply never +// spawns it. +#[cfg(unix)] pub mod admin; pub mod state; +#[cfg(unix)] use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -48,6 +53,12 @@ pub async fn run_async(config: ServerConfig) -> Result<()> { // Optional admin HTTP listener bound to a unix socket. Spawned alongside // the accept loop so a failure to bind / serve doesn't take the tunnel // server down — we just log the error and keep running. + // + // The admin API is unix-socket-only (see `crate::ctl`); on Windows we + // simply never spawn it, even if `admin_socket` is `Some(...)`. The + // caller (`main.rs`) is responsible for warning the operator if they + // explicitly passed `--admin-socket` on Windows. + #[cfg(unix)] let admin_handle: Option> = config .admin_socket .as_ref() @@ -76,14 +87,17 @@ pub async fn run_async(config: ServerConfig) -> Result<()> { info!("shutdown signal received, notifying clients"); endpoint.close(VarInt::from_u32(CLOSE_CODE_SERVER_SHUTDOWN), b"server received ^C"); endpoint.wait_idle().await; - if let Some(h) = admin_handle { - h.abort(); - // The admin task is responsible for unlinking its - // socket file on shutdown; abort()ing while bound is - // OK because it owns nothing the OS won't reap. - } - if let Some(path) = &config.admin_socket { - let _ = std::fs::remove_file(path); + #[cfg(unix)] + { + if let Some(h) = admin_handle { + h.abort(); + // The admin task is responsible for unlinking its + // socket file on shutdown; abort()ing while bound is + // OK because it owns nothing the OS won't reap. + } + if let Some(path) = &config.admin_socket { + let _ = std::fs::remove_file(path); + } } info!("server stopped"); return Ok(()); @@ -140,6 +154,7 @@ pub async fn run_async(config: ServerConfig) -> Result<()> { Ok(()) } +#[cfg(unix)] fn spawn_admin(state: ServerState, path: PathBuf) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { if let Err(e) = admin::serve(state, &path).await { diff --git a/tests/admin.rs b/tests/admin.rs index 621dc44..7434968 100644 --- a/tests/admin.rs +++ b/tests/admin.rs @@ -5,6 +5,11 @@ //! TCP tunnel, and then exercises every `GET /api/v1/...` endpoint over //! the unix socket to verify the JSON shape, byte counters, and history //! recording. +//! +//! Unix-only — the admin API is unix-socket-based and `rusnel::ctl` is +//! not compiled on Windows. + +#![cfg(unix)] mod common;