diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adeb062..26f3ac6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: ${{ runner.os }}-cargo- - name: Build - run: cargo build --release ${{ (runner.os == 'macOS' || runner.os == 'Windows') && '--no-default-features --features tui' || '--features gateway' }} + run: cargo build --release - name: SHA-256 hashes (Linux) if: runner.os == 'Linux' @@ -147,7 +147,7 @@ jobs: uses: taiki-e/install-action@nextest - name: Run unit tests - run: cargo nextest run --all --profile ci --features gateway + run: cargo nextest run --all --profile ci - name: Publish test report (Checks tab) uses: dorny/test-reporter@v2 @@ -197,7 +197,7 @@ jobs: uses: taiki-e/install-action@nextest - name: Run unit tests - run: cargo nextest run --all --profile ci --no-default-features --features tui + run: cargo nextest run --all --profile ci # ───────────────────────────────────────────────────────────────────────────── # Job 2c – Unit tests (Windows) @@ -226,7 +226,7 @@ jobs: uses: taiki-e/install-action@nextest - name: Run unit tests - run: cargo nextest run --all --profile ci --no-default-features --features tui + run: cargo nextest run --all --profile ci # ───────────────────────────────────────────────────────────────────────────── # Job 3 – Integration tests (static mesh + chaos simulation) diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml index 836507e..f51047e 100644 --- a/.github/workflows/package-linux.yml +++ b/.github/workflows/package-linux.yml @@ -90,7 +90,7 @@ jobs: run: cargo install cargo-deb --version 3.6.3 --locked - name: Build release binaries - run: cargo build --release --features gateway + run: cargo build --release - name: Build systemd tarball env: diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml index c2eea4f..c6f27b6 100644 --- a/.github/workflows/package-macos.yml +++ b/.github/workflows/package-macos.yml @@ -86,7 +86,7 @@ jobs: macos-release-${{ matrix.arch }}- - name: Build release binaries - run: cargo build --release --target ${{ matrix.target }} --no-default-features --features tui + run: cargo build --release --target ${{ matrix.target }} - name: Build macOS package run: | diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml index b2469d9..5cd742c 100644 --- a/.github/workflows/package-windows.yml +++ b/.github/workflows/package-windows.yml @@ -77,7 +77,7 @@ jobs: windows-release-x86_64- - name: Build release binaries - run: cargo build --release --no-default-features --features tui + run: cargo build --release - name: Build Windows package shell: pwsh diff --git a/Cargo.toml b/Cargo.toml index accca49..2a3ebc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,10 @@ repository = "https://github.com/jmcorgan/fips" readme = "README.md" [features] -default = ["tui", "ble"] -tui = ["dep:ratatui"] -ble = ["dep:bluer"] -gateway = ["dep:rustables"] +default = [] [dependencies] -ratatui = { version = "0.30", optional = true } +ratatui = "0.30" secp256k1 = { version = "0.30", features = ["rand", "global-context"] } sha2 = "0.10" hkdf = "0.12" @@ -38,15 +35,17 @@ socket2 = { version = "0.6.2", features = ["all"] } tokio-socks = "0.5" portable-atomic = { version = "1", features = ["std"] } -rustables = { version = "0.8.7", optional = true } - [target.'cfg(unix)'.dependencies] tun = { version = "0.8.5", features = ["async"] } libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] rtnetlink = "0.20.0" -bluer = { version = "0.17", features = ["bluetoothd", "l2cap"], optional = true } +rustables = "0.8.7" + +# bluer/BlueZ needs glibc — see build.rs `bluer_available` cfg gate. +[target.'cfg(all(target_os = "linux", not(target_env = "musl")))'.dependencies] +bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] } [target.'cfg(windows)'.dependencies] wintun = "0.5" @@ -95,9 +94,7 @@ path = "src/bin/fipsctl.rs" [[bin]] name = "fips-gateway" path = "src/bin/fips-gateway.rs" -required-features = ["gateway"] [[bin]] name = "fipstop" path = "src/bin/fipstop/main.rs" -required-features = ["tui"] diff --git a/build.rs b/build.rs index ad14543..06ab0d5 100644 --- a/build.rs +++ b/build.rs @@ -36,4 +36,14 @@ fn main() { // Support reproducible builds (Debian packaging) println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH"); + + // bluer/BlueZ is glibc-linux only: musl cross-compiles (OpenWrt) can't + // satisfy libdbus-sys's pkg-config cross-compile requirement, and musl + // router targets don't run BlueZ by default anyway. + println!("cargo:rustc-check-cfg=cfg(bluer_available)"); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); + if target_os == "linux" && target_env != "musl" { + println!("cargo:rustc-cfg=bluer_available"); + } } diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 34a3edd..84aead6 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -29,13 +29,13 @@ build() { cd "$pkgname-$pkgver" export CARGO_TARGET_DIR=target export SOURCE_DATE_EPOCH=$(stat -c %Y Cargo.toml) - cargo build --frozen --release --features gateway + cargo build --frozen --release } check() { cd "$pkgname-$pkgver" export CARGO_TARGET_DIR=target - cargo test --frozen --lib --features gateway + cargo test --frozen --lib } package() { diff --git a/packaging/debian/build-deb.sh b/packaging/debian/build-deb.sh index 13a7aa8..d703044 100755 --- a/packaging/debian/build-deb.sh +++ b/packaging/debian/build-deb.sh @@ -71,7 +71,7 @@ echo "Building .deb package..." OUTPUT_DIR="$(mktemp -d)" trap 'rm -rf "${OUTPUT_DIR}"' EXIT -cargo_args=(deb --output "${OUTPUT_DIR}" --features gateway) +cargo_args=(deb --output "${OUTPUT_DIR}") if [[ -n "${TARGET_TRIPLE}" ]]; then cargo_args+=(--target "${TARGET_TRIPLE}") fi diff --git a/packaging/macos/build-pkg.sh b/packaging/macos/build-pkg.sh index 01a9e10..0974342 100755 --- a/packaging/macos/build-pkg.sh +++ b/packaging/macos/build-pkg.sh @@ -72,7 +72,7 @@ echo "Building FIPS v${VERSION} for macOS ${ARCH}..." # Build release binaries if [[ "${NO_BUILD}" -eq 0 ]]; then - cargo_args=(build --release --manifest-path="${PROJECT_ROOT}/Cargo.toml" --no-default-features --features tui) + cargo_args=(build --release --manifest-path="${PROJECT_ROOT}/Cargo.toml") [[ -n "${TARGET_TRIPLE}" ]] && cargo_args+=(--target "${TARGET_TRIPLE}") cargo "${cargo_args[@]}" fi diff --git a/packaging/openwrt-ipk/build-ipk.sh b/packaging/openwrt-ipk/build-ipk.sh index 2e43565..0bf37b1 100755 --- a/packaging/openwrt-ipk/build-ipk.sh +++ b/packaging/openwrt-ipk/build-ipk.sh @@ -109,8 +109,6 @@ cd "$PROJECT_ROOT" cargo zigbuild \ --release \ --target "$RUST_TARGET" \ - --no-default-features \ - --features tui,gateway \ --bin fips \ --bin fipsctl \ --bin fipstop \ diff --git a/packaging/systemd/build-tarball.sh b/packaging/systemd/build-tarball.sh index af0f72f..7450cf0 100755 --- a/packaging/systemd/build-tarball.sh +++ b/packaging/systemd/build-tarball.sh @@ -84,9 +84,9 @@ fi echo "Building FIPS v${VERSION} for ${ARCH}..." -# Build release binaries (tui is a default feature, includes fipstop) +# Build release binaries if [[ "${NO_BUILD}" -eq 0 ]]; then - cargo_args=(build --release --manifest-path="${PROJECT_ROOT}/Cargo.toml" --features gateway) + cargo_args=(build --release --manifest-path="${PROJECT_ROOT}/Cargo.toml") if [[ -n "${TARGET_TRIPLE}" ]]; then cargo_args+=(--target "${TARGET_TRIPLE}") fi diff --git a/packaging/windows/build-zip.ps1 b/packaging/windows/build-zip.ps1 index fff1620..1b7d6c1 100644 --- a/packaging/windows/build-zip.ps1 +++ b/packaging/windows/build-zip.ps1 @@ -38,7 +38,7 @@ Write-Host "Building FIPS v$Version for Windows $Arch..." # Build release binaries if (-not $NoBuild) { Push-Location $ProjectRoot - cargo build --release --no-default-features --features tui + cargo build --release if ($LASTEXITCODE -ne 0) { Write-Error "cargo build failed" exit 1 diff --git a/src/bin/fips-gateway.rs b/src/bin/fips-gateway.rs index 7b6486b..c4a4a08 100644 --- a/src/bin/fips-gateway.rs +++ b/src/bin/fips-gateway.rs @@ -3,21 +3,33 @@ //! Allows unmodified LAN hosts to reach FIPS mesh destinations via //! DNS-allocated virtual IPs and kernel nftables NAT. +#[cfg(target_os = "linux")] use clap::Parser; +#[cfg(target_os = "linux")] use fips::Config; #[cfg(target_os = "linux")] use fips::gateway::{control, dns, nat, net, pool}; +#[cfg(target_os = "linux")] use fips::version; +#[cfg(target_os = "linux")] use std::path::PathBuf; +#[cfg(target_os = "linux")] use std::sync::Arc; +#[cfg(target_os = "linux")] use std::sync::atomic::{AtomicUsize, Ordering}; +#[cfg(target_os = "linux")] use std::time::Instant; +#[cfg(target_os = "linux")] use tokio::signal::unix::{SignalKind, signal}; +#[cfg(target_os = "linux")] use tokio::sync::{Mutex, mpsc, watch}; +#[cfg(target_os = "linux")] use tracing::{error, info, warn}; +#[cfg(target_os = "linux")] use tracing_subscriber::{EnvFilter, fmt}; /// FIPS outbound LAN gateway +#[cfg(target_os = "linux")] #[derive(Parser, Debug)] #[command( name = "fips-gateway", @@ -35,6 +47,13 @@ struct Args { log_level: String, } +#[cfg(not(target_os = "linux"))] +fn main() { + eprintln!("fips-gateway requires Linux (nftables unavailable on this platform)"); + std::process::exit(1); +} + +#[cfg(target_os = "linux")] #[tokio::main(flavor = "current_thread")] async fn main() { let args = Args::parse(); diff --git a/src/config/mod.rs b/src/config/mod.rs index 53b27d3..cdf6eb4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -18,7 +18,7 @@ //! nsec: "nsec1..." //! ``` -#[cfg(feature = "gateway")] +#[cfg(target_os = "linux")] mod gateway; mod node; mod peer; @@ -30,7 +30,7 @@ use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use thiserror::Error; -#[cfg(feature = "gateway")] +#[cfg(target_os = "linux")] pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig, PortForward, Proto}; pub use node::{ BloomConfig, BuffersConfig, CacheConfig, ControlConfig, DiscoveryConfig, LimitsConfig, @@ -378,7 +378,7 @@ pub struct Config { pub peers: Vec, /// Gateway configuration (`gateway`). - #[cfg(feature = "gateway")] + #[cfg(target_os = "linux")] #[serde(default, skip_serializing_if = "Option::is_none")] pub gateway: Option, } @@ -500,7 +500,7 @@ impl Config { self.peers = other.peers; } // Merge gateway section — higher-priority config replaces entirely - #[cfg(feature = "gateway")] + #[cfg(target_os = "linux")] if other.gateway.is_some() { self.gateway = other.gateway; } diff --git a/src/lib.rs b/src/lib.rs index 8e0bdd9..34ff472 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ pub mod bloom; pub mod cache; pub mod config; pub mod control; -#[cfg(feature = "gateway")] +#[cfg(target_os = "linux")] pub mod gateway; pub mod identity; pub mod mmp; diff --git a/src/node/lifecycle.rs b/src/node/lifecycle.rs index 2a9161c..3abd992 100644 --- a/src/node/lifecycle.rs +++ b/src/node/lifecycle.rs @@ -125,7 +125,7 @@ impl Node { } } } else if addr.transport == "ble" { - #[cfg(target_os = "linux")] + #[cfg(bluer_available)] { match self.resolve_ble_addr(&addr.addr) { Ok(result) => result, @@ -140,11 +140,11 @@ impl Node { } } } - #[cfg(not(target_os = "linux"))] + #[cfg(not(bluer_available))] { debug!( transport = %addr.transport, - "BLE transport not available on this platform" + "BLE transport not available on this build" ); continue; } diff --git a/src/node/mod.rs b/src/node/mod.rs index ce625a0..f16c7fc 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -800,7 +800,7 @@ impl Node { } // Create BLE transport instances - #[cfg(target_os = "linux")] + #[cfg(bluer_available)] { let ble_instances: Vec<_> = self .config @@ -810,7 +810,7 @@ impl Node { .map(|(name, config)| (name.map(|s| s.to_string()), config.clone())) .collect(); - #[cfg(all(feature = "ble", not(test)))] + #[cfg(all(bluer_available, not(test)))] for (name, ble_config) in ble_instances { let transport_id = self.allocate_transport_id(); let adapter = ble_config.adapter().to_string(); @@ -833,12 +833,10 @@ impl Node { } } - #[cfg(any(not(feature = "ble"), test))] + #[cfg(any(not(bluer_available), test))] if !ble_instances.is_empty() { #[cfg(not(test))] - tracing::warn!( - "BLE transport configured but 'ble' feature not enabled at compile time" - ); + tracing::warn!("BLE transport configured but this build lacks BlueZ support"); } } @@ -908,7 +906,7 @@ impl Node { /// Resolve a BLE address string (`"adapter/AA:BB:CC:DD:EE:FF"`) to a /// (TransportId, TransportAddr) pair by finding the BLE transport /// instance matching the adapter name. - #[cfg(target_os = "linux")] + #[cfg(bluer_available)] fn resolve_ble_addr(&self, addr_str: &str) -> Result<(TransportId, TransportAddr), NodeError> { let ta = TransportAddr::from_string(addr_str); let adapter = crate::transport::ble::addr::adapter_from_addr(&ta).ok_or_else(|| { diff --git a/src/transport/ble/addr.rs b/src/transport/ble/addr.rs index bfbe77e..1b7ed58 100644 --- a/src/transport/ble/addr.rs +++ b/src/transport/ble/addr.rs @@ -55,10 +55,10 @@ impl BleAddr { } // ============================================================================ -// bluer type conversions (behind ble feature) +// bluer type conversions (glibc-linux only; see build.rs bluer_available) // ============================================================================ -#[cfg(feature = "ble")] +#[cfg(bluer_available)] impl BleAddr { /// Construct from a bluer `Address` and adapter name. pub fn from_bluer(addr: bluer::Address, adapter: &str) -> Self { diff --git a/src/transport/ble/io.rs b/src/transport/ble/io.rs index 454ea95..16c6c5c 100644 --- a/src/transport/ble/io.rs +++ b/src/transport/ble/io.rs @@ -1,7 +1,7 @@ //! BLE I/O abstraction layer. //! //! Defines the `BleIo` trait that separates transport logic from the -//! BlueZ/bluer stack. `BluerIo` (behind `cfg(feature = "ble")`) provides +//! BlueZ/bluer stack. `BluerIo` (behind `cfg(bluer_available)`) provides //! the real implementation; `MockBleIo` provides an in-memory test double. use crate::transport::TransportError; @@ -109,7 +109,7 @@ pub trait BleIo: Send + Sync + 'static { // BluerIo — Production BLE I/O via BlueZ D-Bus // ============================================================================ -#[cfg(feature = "ble")] +#[cfg(bluer_available)] mod bluer_impl { use super::*; use crate::transport::TransportError; @@ -486,7 +486,7 @@ mod bluer_impl { } } -#[cfg(feature = "ble")] +#[cfg(bluer_available)] pub use bluer_impl::{BluerAcceptor, BluerIo, BluerScanner, BluerStream, FIPS_SERVICE_UUID}; // ============================================================================ diff --git a/src/transport/ble/mod.rs b/src/transport/ble/mod.rs index 842037b..9561dae 100644 --- a/src/transport/ble/mod.rs +++ b/src/transport/ble/mod.rs @@ -9,7 +9,7 @@ //! //! Transport logic (pool, discovery, lifecycle) is separated from the //! BlueZ/bluer stack via the `BleIo` trait. `BluerIo` provides the real -//! implementation (behind `cfg(feature = "ble")`); `MockBleIo` provides +//! implementation (behind `cfg(bluer_available)`); `MockBleIo` provides //! an in-memory test double for CI without hardware. //! //! ## Connection Pool @@ -50,12 +50,12 @@ pub const DEFAULT_PSM: u16 = 0x0085; /// Concrete BLE transport type for use in TransportHandle. /// -/// Production builds with the `ble` feature use `BluerIo` (real BlueZ stack). -/// Test builds and builds without `ble` use `MockBleIo`. -#[cfg(all(feature = "ble", not(test)))] +/// Production builds on glibc-linux use `BluerIo` (real BlueZ stack). +/// Test builds, musl-linux, and non-Linux platforms use `MockBleIo`. +#[cfg(all(bluer_available, not(test)))] pub type DefaultBleTransport = BleTransport; -#[cfg(any(not(feature = "ble"), test))] +#[cfg(any(not(bluer_available), test))] pub type DefaultBleTransport = BleTransport; // ============================================================================ diff --git a/testing/ci-local.sh b/testing/ci-local.sh index b873b38..222eae4 100755 --- a/testing/ci-local.sh +++ b/testing/ci-local.sh @@ -149,8 +149,8 @@ record() { run_build() { stage "Stage 1: Build" - info "cargo build --release --features gateway" - if cargo build --release --features gateway 2>&1; then + info "cargo build --release" + if cargo build --release 2>&1; then record "build" 0 else record "build" 1 @@ -165,8 +165,8 @@ run_build() { return 1 fi - info "cargo clippy --all --features gateway -- -D warnings" - if cargo clippy --all --features gateway -- -D warnings 2>&1; then + info "cargo clippy --all -- -D warnings" + if cargo clippy --all -- -D warnings 2>&1; then record "clippy" 0 else record "clippy" 1 @@ -181,7 +181,7 @@ run_tests() { local cmd if command -v cargo-nextest &>/dev/null; then - cmd="cargo nextest run --all --features gateway" + cmd="cargo nextest run --all" info "$cmd" if $cmd 2>&1; then record "unit-tests" 0 @@ -189,7 +189,7 @@ run_tests() { record "unit-tests" 1 fi else - cmd="cargo test --all --features gateway" + cmd="cargo test --all" info "$cmd (nextest not found, using cargo test)" if $cmd 2>&1; then record "unit-tests" 0