Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -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"]
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 2 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PATH>`
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.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,10 @@ sharing <remote-host>:<remote-port> from the client to the server\'s <local-host
/// /tmp/rusnel-admin-<uid>.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")]
Expand All @@ -477,6 +481,7 @@ sharing <remote-host>:<remote-port> from the client to the server\'s <local-host
},
}

#[cfg(unix)]
#[derive(Debug, Subcommand)]
enum CtlAction {
/// Print server info: version, listen address, uptime, client count.
Expand Down Expand Up @@ -1196,11 +1201,28 @@ fn main() {
// — opt out with `--no-admin-socket`, override with
// `--admin-socket <PATH>`. 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);
Expand Down Expand Up @@ -1347,6 +1369,7 @@ fn main() {
debug!(?client_config, "client config resolved");
run_client(client_config);
}
#[cfg(unix)]
Mode::Ctl {
socket,
json,
Expand Down Expand Up @@ -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 };
Expand Down
31 changes: 23 additions & 8 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<tokio::task::JoinHandle<()>> = config
.admin_socket
.as_ref()
Expand Down Expand Up @@ -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(());
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions tests/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading