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
72 changes: 37 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ concurrency:
cancel-in-progress: false

env:
UV_PYTHON_PREFERENCE: only-system
UV_PYTHON_DOWNLOADS: never
# Reusable calls inherit the caller's event and workflow context.
VOLE_RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_type == 'tag' && github.workflow_ref == format('{0}/.github/workflows/release.yml@{1}', github.repository, github.ref) && github.ref_name || '' }}
VOLE_RELEASE_REPOSITORY: ${{ github.repository }}
Expand Down Expand Up @@ -72,16 +74,14 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/setup-python@v7
with:
python-version: "3.x"
check-latest: true
architecture: ${{ runner.arch == 'ARM64' && 'arm64' || 'x64' }}
- uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12"
- name: Install latest stable Python 3
shell: bash
env:
UV_PYTHON_INSTALL_DIR: ${{ runner.temp }}/python
run: |
uv python install 3 --no-bin --no-registry
echo "UV_PYTHON=$(uv python find --managed-python --no-project 3)" >> "$GITHUB_ENV"
- name: Linux native dependencies
if: matrix.platform == 'linux'
run: |
Expand Down Expand Up @@ -188,16 +188,14 @@ jobs:
with:
toolchain: stable
targets: ${{ matrix.targets }}
- uses: actions/setup-python@v7
with:
python-version: "3.x"
check-latest: true
architecture: ${{ runner.arch == 'ARM64' && 'arm64' || 'x64' }}
- uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12"
- name: Install latest stable Python 3
shell: bash
env:
UV_PYTHON_INSTALL_DIR: ${{ runner.temp }}/python
run: |
uv python install 3 --no-bin --no-registry
echo "UV_PYTHON=$(uv python find --managed-python --no-project 3)" >> "$GITHUB_ENV"
- name: Locked source
run: cargo fetch --locked
- name: Linux native dependencies
Expand Down Expand Up @@ -244,6 +242,19 @@ jobs:
if [[ -n "$VOLE_RELEASE_TAG" ]]; then args+=(--tag "$VOLE_RELEASE_TAG"); fi
uv run --project scripts --locked python -m vole_scripts.release "${args[@]}"
test -f "dist/release/$VOLE_GROUP/$VOLE_ARCHIVE"
- name: Check unpackaged Windows COM lifecycle
if: matrix.platform == 'windows' && matrix.backend == 'uwp'
shell: bash
env:
VOLE_TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
case "$VOLE_TARGET" in
x86_64-pc-windows-msvc) architecture=x64 ;;
aarch64-pc-windows-msvc) architecture=arm64 ;;
*) exit 1 ;;
esac
python scripts/check_windows_com.py --dll "dist/windows/$architecture/uwp/vole.dll"
- name: Upload release archive
uses: actions/upload-artifact@v7
with:
Expand Down Expand Up @@ -280,16 +291,13 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/setup-python@v7
with:
python-version: "3.x"
check-latest: true
- uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12"
- name: Install latest stable Python 3
shell: bash
env:
UV_PYTHON_INSTALL_DIR: ${{ runner.temp }}/python
run: |
uv python install 3 --no-bin --no-registry
echo "UV_PYTHON=$(uv python find --managed-python --no-project 3)" >> "$GITHUB_ENV"
- name: Cache native Rust dependencies
uses: Swatinem/rust-cache@v2
with:
Expand Down Expand Up @@ -323,16 +331,13 @@ jobs:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.x"
check-latest: true
- uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12"
- name: Install latest stable Python 3
shell: bash
env:
UV_PYTHON_INSTALL_DIR: ${{ runner.temp }}/python
run: |
uv python install 3 --no-bin --no-registry
echo "UV_PYTHON=$(uv python find --managed-python --no-project 3)" >> "$GITHUB_ENV"
- uses: actions/download-artifact@v8
with:
pattern: vole-apple-*-${{ github.sha }}
Expand All @@ -359,16 +364,13 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.x"
check-latest: true
- uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12"
- name: Install latest stable Python 3
shell: bash
env:
UV_PYTHON_INSTALL_DIR: ${{ runner.temp }}/python
run: |
uv python install 3 --no-bin --no-registry
echo "UV_PYTHON=$(uv python find --managed-python --no-project 3)" >> "$GITHUB_ENV"
- uses: actions/download-artifact@v8
with:
pattern: vole-release-build-*-${{ github.sha }}
Expand Down
15 changes: 8 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
UV_PYTHON_PREFERENCE: only-system
UV_PYTHON_DOWNLOADS: never

jobs:
client-contracts:
name: ${{ matrix.name }}
Expand All @@ -33,16 +37,13 @@ jobs:
with:
toolchain: stable
components: rustfmt, clippy
- uses: actions/setup-python@v7
with:
python-version: "3.x"
check-latest: true
- uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12"
- name: Install latest stable Python 3
shell: bash
env:
UV_PYTHON_INSTALL_DIR: ${{ runner.temp }}/python
run: |
uv python install 3 --no-bin --no-registry
echo "UV_PYTHON=$(uv python find --managed-python --no-project 3)" >> "$GITHUB_ENV"
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
Expand Down
4 changes: 4 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ _Avoid_: Plugin, background task when referring to the whole participant, proxy
The desktop raw-IP platform adapter backed by a host-provided Wintun DLL. It connects to the shared Vole TUN runtime without a Windows VPN Provider, package identity or packet channel; the first release leaves system network configuration to the host.
_Avoid_: Windows VPN provider, second proxy core, Unix fd emulation

**Windows VPN profile**:
The Windows system VPN profile selected by the host's stable `profileName` within its package family. Start, status and stop use the same exact name; omission selects `Vole`. A custom name does not enable concurrent package sessions or migrate another profile.
_Avoid_: Proxy node, user configuration, Session Snapshot

**Windows VPN policy**:
The profile-level Always On and destination-bypass choices for a Windows tunnel whose application scope is always global.
_Avoid_: Per-app policy, app selection, traffic filter
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ windows = { version = "=0.62.2", optional = true, features = [
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Isolation",
"Win32_System_Com",
"Win32_System_JobObjects",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
Expand Down
58 changes: 55 additions & 3 deletions docs/invoke-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ void VoleFree(char *response);
- 业务运行时线程不能重入 Invoke;Debug 和 Release 构建都立即返回失败 JSON,包括 `version` 等只读请求。
- 请求正文、响应正文、完整配置、UUID、密钥、short ID 和凭据不得写入日志。
- UWP 宿主包含 `vole_windows_uwp.h`(同时引入 `vole.h`),其中声明 `VoleWindowsVpnInvoke`。该头文件和独立的 Windows 安装包桥接接口仅由 `windows-uwp` 构建提供。
- `VoleWindowsVpnInvoke` 当前在调用线程上初始化 MTA;调用线程必须尚未初始化 COM,或已经是 MTA。STA/ASTA 调用不受支持。
- `VoleWindowsVpnInvoke` 接受未显式初始化 COM 或已是 MTA 的调用线程;STA/ASTA 调用失败且不改变其 apartment。库配对管理每次调用的初始化,并为静态 WinRT factory 缓存保留一份进程寿命的 MTA 引用;宿主无需额外持有 MTA,短生命周期工作线程可串行重复调用。返回后未显式初始化的线程可能处于 implicit MTA,不遗留逐次显式初始化计数;完整边界见 [COM / WinRT 生命周期](windows-vpn.md#com--winrt-生命周期)。

## 请求与响应

Expand Down Expand Up @@ -345,6 +345,30 @@ Unix 等待 SIGINT/SIGTERM,Windows 等待 Ctrl+C/Ctrl+Break;标准输入先

## Android protect

Android JNI 类固定为 `io.github.yuandevteam.vole.NativeVole`,与宿主应用的包名独立。
宿主提供如下 Java 声明(或等价的 Kotlin/JVM 声明),并打包匹配 ABI 的原生库:

```java
package io.github.yuandevteam.vole;

public final class NativeVole {
static {
System.loadLibrary("vole");
}

private NativeVole() {}

public static native byte[] nativeInvoke(byte[] request);
public static native boolean nativeRegisterProtectController(Object controller);
public static native boolean nativeUnregisterProtectController();
}
```

仅导出该命名空间下的三个 JNI 方法,不提供其它包名别名。宿主声明必须与原生库一致;
代码压缩和混淆必须保留该类、这些 native 方法以及 controller 的 `boolean protect(int)`
方法名。`nativeInvoke` 的请求与响应均为标准 UTF-8 JSON `byte[]`,由 JVM 管理,
不使用 C ABI 的 `VoleFree` 释放。

Android TUN 通过 Invoke 之外的运行时本地回调注册:

```text
Expand Down Expand Up @@ -385,11 +409,37 @@ ProtectFd(fd) -> bool
- `getStartupTaskStatus`
- `setStartupTaskEnabled`

### VPN profile 名称

`startVpn`、`getVpnStatus` 和 `stopVpn` 的 payload 都接受可选的 `profileName`。
省略时使用 `Vole`;使用自定义名称的宿主必须在这三个方法中始终传入同一个值,包括
前台重启后的查询。名称不属于业务 YAML、Session Snapshot 或 profile custom configuration,
不改变 Session token 的计算。

```json
{"bridgeVersion":3,"method":"getVpnStatus","payload":{"profileName":"Example VPN"}}
```

- 名称必须是 1–256 UTF-8 字节的字符串,不能有首尾 Unicode 空白或控制字符(含 NUL)。
`null`、空串和其它 JSON 类型都拒绝,不 trim、不补默认值;其它系统名称限制仍由 Windows 返回错误。
- 仅匹配当前 package family 内同名的 VPN profile,区分大小写且不做 Unicode normalization。
其它 package 的 profile 不会被接管或修改;Windows 名称冲突不会触发删除、重命名或回退。
- 同包存在其它名称的活动 profile 时,三个方法均失败,而不是报告已断开或操作那个 profile;
多个同名匹配同样失败。同包不同名且已断开的 profile 不参与本次操作。
- 宿主应使用稳定名称。这不是多 profile 管理接口,不支持多会话并行、自动迁移、重命名或删除。
更换名称前必须用旧名称显式 `stopVpn` 并确认断开;使用新名称启动不会复用或删除旧名称的 profile。
名称选择不会转换旧 Session token 或解除现有 profile 配置校验。
- `getEnvironment`、`getStartupTaskStatus` 和 `setStartupTaskEnabled` 不接受 `profileName`。
StartupTask ID 仍固定为 `VoleStartup`,与 VPN profile 名称无关。

### 启动 payload

`startVpn` 的 payload 为:

```json
{
"configYaml": "tun:\n enable: true\n...",
"profileName": "Example VPN",
"configYaml": "tun:\n enable: true\n mtu: 1400\n...",
"networkSettings": {
"ipv4Address": "192.168.3.1",
"ipv6Address": "fd00::2",
Expand Down Expand Up @@ -418,7 +468,7 @@ ProtectFd(fd) -> bool

桥接把 YAML、进程顺序、路径和参数发布为 `vole-session-v2:<sha256>` Session Snapshot。参数引用的文件由调用方保持存在且不可变,Vole 不读取或摘要其内容。`getVpnStatus.data.snapshotToken` 返回该完整 Session token。

桥接请求最大 1 MiB。它负责安装包身份、单一 VPN profile、不可变 Session Snapshot、连接/断开命令和系统 VPN 状态;Provider 负责激活 Session Host。桥接不公开 profile CRUD、内部文件路径、backend 描述、参数、PID、管道名称或 Snapshot 维护。数据包、Controller 流量查询、代理组查询/切换和业务生命周期不经过该 JSON 桥接。
桥接请求最大 1 MiB。它负责安装包身份、按名称选择的 VPN profile、不可变 Session Snapshot、连接/断开命令和系统 VPN 状态;Provider 负责激活 Session Host。桥接不公开 profile CRUD、内部文件路径、backend 描述、参数、PID、管道名称或 Snapshot 维护。数据包、Controller 流量查询、代理组查询/切换和业务生命周期不经过该 JSON 桥接。

## 编码与安全边界

Expand All @@ -428,3 +478,5 @@ ProtectFd(fd) -> bool
- 嵌套 UDP 协议可以增加有界帧头,但解封装后的最终负载仍受该平台响应上限限制。
- 节点和代理组定义名共享大小写敏感的严格 UTF-8 命名空间;`DIRECT`、`REJECT` 和 `RULES` 不能用作定义名。
- Secret、password、UUID、REALITY key、short ID、目标地址和完整配置不得进入日志。
- Apple Unified Logging 的 subsystem 固定为 `io.github.yuandevteam.vole`,category 为
`vole`,不继承宿主应用的 bundle identifier;Console 或 `log stream` 应按该标识筛选。
23 changes: 19 additions & 4 deletions docs/windows-vpn.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,20 @@ tun-rs 的 Wintun 后端,也不依赖外置 `wintun.dll`。Wintun 的 interrup

未安装的普通桌面进程没有 package identity,Windows VPN 桥接会失败关闭。

`VoleWindowsVpnInvoke` 当前在调用线程上初始化 MTA。调用线程必须尚未初始化 COM,或已经是 MTA;STA/ASTA 调用不受支持。
### COM / WinRT 生命周期

`VoleWindowsVpnInvoke` 在调用线程上配对执行 `RoInitialize(MTA)` / `RoUninitialize`。
调用线程可以尚未显式初始化 COM,或已经是 MTA;STA/ASTA 调用失败,不改变调用方的 apartment。

桥接首次成功进入 MTA 时,通过 `CoIncrementMTAUsage` 保留一份进程寿命的 MTA 引用,
与 `windows-rs` 的静态 agile factory 缓存同寿命。即使调用线程退出、调用方释放自己的
MTA,或两次请求之间没有显式 MTA 线程,缓存也不会跨越 MTA 拆卸后被再次使用。
这是一份固定引用,不随请求增长;不在逐次返回或 DLL/process teardown 时释放,
也不在 loader lock 内执行 COM 清理。获取失败仍配对释放本次初始化,并返回失败。

宿主无需额外持有 MTA,可以从不同的短生命周期工作线程串行调用。返回后,原本未显式
初始化的线程可能属于进程的 implicit MTA;桥接不会遗留该线程的显式初始化计数。
命令仍不允许重叠,不增加工作队列,也不改变包身份、profile 或会话生命周期。

## `IVpnPlugIn` 回调

Expand Down Expand Up @@ -171,7 +184,7 @@ vole-windows-session-host.exe
- Session Host 是 `windows.fullTrustProcess` extension,不显示在应用列表,也不注册 StartupTask 或 URI;
- Provider 的 `windows.backgroundTasks` extension 显式使用 `windowsApp + appContainer`;
- Provider activation class 来自 `vole.dll`;
- 同一 package 只维护一个 `Vole` VPN profile;
- 宿主为同一 package 使用一个稳定的 VPN profile 名称;`startVpn`、`getVpnStatus`、`stopVpn` 的可选 `profileName` 缺省为 `Vole`,匹配和改名边界见 [Invoke API](invoke-api.md#vpn-profile-名称)。同包仍只允许一个活动会话;
- custom configuration 是最大 4 KiB 的严格 JSON,只含修订版 4、Session token、顶层 IPv6 开关、四个网络地址和完整 policy;
- Session Snapshot 是 `LocalState/vole/windows/sessions/<sha256>.json`,revision 2 覆盖完整 YAML、可选进程顺序、路径和参数;读取验证大小、普通文件、reparse point、规范 JSON、摘要和每个 executable。参数引用的文件由宿主保持存在且不可变,Vole 不读取其内容;
- 活动 Session token、IPv6 开关、网络地址或 policy 不同时必须先显式 Stop,不能热切换;
Expand Down Expand Up @@ -233,9 +246,9 @@ Session Host 每次连接新建一个进程,不常驻、不复用运行时,

## 启动顺序

1. 前台宿主调用 `startVpn(configYaml, networkSettings, policy, sessionBackend?)`。
1. 前台宿主调用 `startVpn(configYaml, networkSettings, policy, sessionBackend?, profileName?)`,后续查询和停止使用同一 profile 名称。
2. 桥接验证配置、四个地址、policy 和进程描述,发布不可变 Session Snapshot,并把解析后的顶层 IPv6 开关和 policy 写入 profile configuration。
3. 桥接写入单一 VPN profile 并调用 `ConnectProfileAsync`;它不启动或持有 Session Host。
3. 桥接按当前 package family 和请求的名称查找并写入 VPN profile,再调用 `ConnectProfileAsync`;它不接管其它 profile,也不启动或持有 Session Host。
4. Windows 激活 AppContainer Provider。
5. Provider 从 profile configuration 取得权威 token,选择物理网络绑定并准备基础资源。
6. Provider 清理陈旧会合记录,通过无参数 `FullTrustProcessLauncher` 激活 Session Host。
Expand Down Expand Up @@ -347,3 +360,5 @@ Stopped { version, packetCounters }
- [`VpnManagementAgent`](https://learn.microsoft.com/uwp/api/windows.networking.vpn.vpnmanagementagent)
- [`FullTrustProcessLauncher`](https://learn.microsoft.com/uwp/api/windows.applicationmodel.fulltrustprocesslauncher)
- [Package identity](https://learn.microsoft.com/windows/apps/desktop/modernize/package-identity-overview)
- [`RoInitialize`](https://learn.microsoft.com/windows/win32/api/roapi/nf-roapi-roinitialize)
- [`CoIncrementMTAUsage`](https://learn.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coincrementmtausage)
Loading
Loading