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
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: cargo test

# The host clippy compiles the Apple-gated modules (src/ffi.rs,
# src/tunnel/ios.rs), which Linux CI never type-checks.
# src/tunnel/mobile.rs), which Linux CI never type-checks.
macos:
runs-on: macos-latest
steps:
Expand All @@ -43,6 +43,27 @@ jobs:
- name: Build Apple xcframework + header (debug)
run: ./build-apple.sh debug

# Clippy for the Android-gated modules (src/ffi_android.rs and the Android
# branches of src/ffi.rs / src/tunnel/mobile.rs), plus a verify-only build of
# libezvpn.so for every release ABI (debug profile for speed), mirroring the
# release workflow's build-android-lib job. Nothing is published.
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Install cargo-ndk
run: cargo install cargo-ndk --locked
- name: Clippy (arm64-v8a)
run: |
export ANDROID_NDK_HOME="${ANDROID_NDK_LATEST_HOME:?runner image has no Android NDK}"
rustup target add aarch64-linux-android
cargo ndk -t arm64-v8a --platform 29 clippy --lib -- -D warnings
- name: Build libezvpn.so (debug)
run: |
export ANDROID_NDK_HOME="${ANDROID_NDK_LATEST_HOME:?runner image has no Android NDK}"
ABIS="arm64-v8a armeabi-v7a x86_64" ./build-android.sh debug

# The host clippy compiles the Windows-gated module (src/ffi_windows.rs).
windows:
runs-on: windows-latest
Expand Down
25 changes: 23 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,27 @@ jobs:
name: release-apple
path: dist/apple/libezvpn-apple.xcframework.zip

build-android-lib:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Install cargo-ndk
run: cargo install cargo-ndk --locked
- name: Build libezvpn.so per ABI + zip
# build-android.sh installs the Android Rust targets if missing, builds
# one libezvpn.so per ABI with the runner's preinstalled NDK, and stages
# dist/android/jniLibs + libezvpn-android.zip. That zip becomes the
# release asset the Android app's Gradle build downloads by default.
run: |
export ANDROID_NDK_HOME="${ANDROID_NDK_LATEST_HOME:?runner image has no Android NDK}"
./build-android.sh release
- uses: actions/upload-artifact@v4
with:
name: release-android
path: dist/android/libezvpn-android.zip

build-windows:
needs: setup
if: needs.setup.outputs.is_prerelease == 'false'
Expand Down Expand Up @@ -175,8 +196,8 @@ jobs:
path: dist/windows/ezvpn-windows.dll.zip

publish-release:
needs: [setup, build-linux, build-macos, build-apple, build-windows, build-windows-lib]
if: always() && needs.setup.result == 'success' && needs.build-linux.result == 'success' && needs.build-macos.result == 'success' && needs.build-apple.result == 'success' && needs.build-windows-lib.result == 'success' && (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped')
needs: [setup, build-linux, build-macos, build-apple, build-android-lib, build-windows, build-windows-lib]
if: always() && needs.setup.result == 'success' && needs.build-linux.result == 'success' && needs.build-macos.result == 'success' && needs.build-apple.result == 'success' && needs.build-android-lib.result == 'success' && needs.build-windows-lib.result == 'success' && (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped')
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ address lookup, the per-relay startup probe, relay auth tokens, relay
self-hosting β€” is documented once in
https://github.com/flexaccessdev/iroh-common-architecture. Do not duplicate it in
this repo; update it there and link to it.

The mobile apps live in sibling repos: `../ezvpn-apple` (Swift, see
`docs/Apple-App.md`) and `../ezvpn-android` (Kotlin, see `docs/Android-App.md`).
Both drive the fd-based `MobileSession` in `src/tunnel/mobile.rs` through
`src/ffi.rs`; Android adds the JNI layer `src/ffi_android.rs`, whose symbol
names are bound to the Kotlin class `dev.flexaccess.ezvpn.EzvpnNative` β€” do not
rename either side alone. The in-tunnel split-DNS forwarder
(`src/tunnel/dns_proxy.rs`) is an Android-only workaround for the platform
having no per-domain VPN DNS; every other platform keeps OS-level conditional
forwarding, so never wire it up elsewhere. Verify Android changes with
`cargo ndk -t arm64-v8a --platform 29 clippy --lib -- -D warnings` (the module
is cfg-gated out of the host clippy).
34 changes: 32 additions & 2 deletions Cargo.lock

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

19 changes: 15 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ezvpn"
version = "0.0.40"
version = "0.0.41"
edition = "2024"
description = "IP-over-QUIC VPN tunnel via iroh P2P"
readme = "README.md"
Expand All @@ -12,6 +12,8 @@ name = "ezvpn"
# staticlib: linked into Apple Network Extensions (iOS and native macOS).
# cdylib: `ezvpn.dll`, P/Invoked by the native Windows GUI (`ezvpn-windows`)
# via the C FFI in `src/ffi_windows.rs`. Built by `build-windows.ps1`.
# Also `libezvpn.so` for Android, loaded by the `ezvpn-android` app through
# the JNI surface in `src/ffi_android.rs`. Built by `build-android.sh`.
crate-type = ["rlib", "staticlib", "cdylib"]

[dependencies]
Expand Down Expand Up @@ -75,11 +77,20 @@ windows-sys = { version = "0.61", features = [
] }

# Desktop-only: on-link subnet enumeration for the connect-time split-tunnel
# overlap refusal (src/net/local_networks.rs). iOS has its own Swift port of
# this check in ezvpn-apple TunnelCore.
[target.'cfg(not(target_os = "ios"))'.dependencies]
# overlap refusal (src/net/local_networks.rs). The mobile apps carry their own
# ports of this check (ezvpn-apple TunnelCore, ezvpn-android tunnelcore).
[target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies]
if-addrs = "0.15"

# Android-only: the JNI bridge for the `ezvpn-android` VpnService, and logcat
# output for `log` (stderr is discarded on Android).
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
android_logger = "0.15"
# iroh's Android DNS/interface discovery (hickory-resolver, netwatch) reaches
# the JVM through this global; the app registers its Context via `EzvpnNative.init`.
ndk-context = "0.1"

[dev-dependencies]
mock_instant = "0.6"
rand_chacha = "0.10"
Expand Down
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ that bridges two sites with stable subnets, WireGuard is the right choice, not
client do one thing β€” tunneling. Firewall, forwarding/NAT, and DNS
configuration (e.g. conditional forwarding for an internal zone, see
[docs/Client-Split-DNS.md](docs/Client-Split-DNS.md)) are expected to be
managed outside the VPN connector. The iOS app is the one deliberate
exception: it applies split DNS in-app (`NEDNSSettings`), because on iOS
that is the only way to accomplish it.
managed outside the VPN connector. The mobile apps are the deliberate
exception: iOS applies split DNS in-app (`NEDNSSettings`) because that is the
only way to accomplish it there, and Android forwards DNS in-tunnel because the
platform has no per-domain DNS for VPNs at all.

Also do not use `ezvpn` when the goal is anonymity. iroh's relays can see relay
metadata when they are involved, even though the VPN payload remains encrypted.
Expand Down Expand Up @@ -622,9 +623,11 @@ Two things you do **not** need firewall rules for:
On the client side, DNS is likewise managed outside the tunnel: to resolve an
internal zone through a resolver reachable over the VPN, set OS-level
conditional forwarding on each client β€” see
[docs/Client-Split-DNS.md](docs/Client-Split-DNS.md). The exception is iOS,
where the app applies DNS conditional forwarding in-tunnel itself (see
[docs/Apple-App.md](docs/Apple-App.md)).
[docs/Client-Split-DNS.md](docs/Client-Split-DNS.md). The exceptions are the
mobile apps: iOS applies DNS conditional forwarding through `NEDNSSettings`
(see [docs/Apple-App.md](docs/Apple-App.md)), and Android β€” which has no
split-DNS API for VPNs β€” runs an in-tunnel forwarder in the Rust core (see
[docs/Android-App.md](docs/Android-App.md)).

## Protocol, MTU, and GSO

Expand Down Expand Up @@ -778,6 +781,20 @@ Swift app consumes via a Swift package binary target.
See [`docs/Apple-App.md`](docs/Apple-App.md) for scope, how it reuses the core, the C
interface, and build steps.

## Android App

[`ezvpn-android`](https://github.com/flexaccessdev/ezvpn-android) is a native
Kotlin/Compose client for Android that connects to an `ezvpn` server built from
this repo (dual-stack split tunnel, optional tunnel DNS including split-DNS
match domains via an in-tunnel forwarder, always-on support; no full tunnel or
Play Store distribution). The tunnel runs in a `VpnService` that is handed the
OS tun fd, like the Apple extension. The Rust core builds into one
`libezvpn.so` per ABI here (`./build-android.sh`, released as
`libezvpn-android.zip`), which the app loads through a small JNI surface.

See [`docs/Android-App.md`](docs/Android-App.md) for scope, how it reuses the
core, the JNI interface, the split-DNS forwarder, and build steps.

## Windows App

[`ezvpn-windows`](https://github.com/flexaccessdev/ezvpn-windows) is a native
Expand Down
Loading
Loading