Skip to content
Closed
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
52 changes: 31 additions & 21 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Android

# Builds the Android APKs: the Rust server plus the EasyTier and frp tunnel
# clients, all cross-compiled from source with the NDK, packed into a small
# app that runs them as a foreground service.
# 构建安卓 APK(手机上运行的 VIVANT 服务器 + EasyTier / frp 内网穿透)。
# Builds the Android APKs: the Rust server and the EasyTier / frp clients
# cross-compiled from source with the NDK, plus Tor (tor-android, as in
# Orbot), packed into a small app that runs them as a foreground service.
# 构建安卓 APK(手机上运行的 VIVANT 服务器 + Tor / EasyTier / frp)。
#
# Stable signing (so updates install over each other): add the repository
# secrets ANDROID_KEYSTORE_BASE64 (base64 of a .jks), ANDROID_KEYSTORE_PASSWORD,
Expand Down Expand Up @@ -44,7 +44,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: android/.tunnel-src/easytier -> target
key: easytier-${{ env.EASYTIER_VERSION }}-${{ matrix.abi }}
key: easytier-${{ env.EASYTIER_VERSION }}-${{ matrix.abi }}-${{ hashFiles('android/easytier-no-ip.patch') }}
- name: Build
run: |
set -o pipefail
Expand Down Expand Up @@ -75,14 +75,14 @@ jobs:
run: |
set -o pipefail
android/build-tunnels.sh frpc 2>&1 | tee build.log
file android/app/src/main/jniLibs/*/libfrpc.so
file android/app/src/full/jniLibs/*/libfrpc.so
- name: Report failure
if: failure()
run: .github/report-failure.sh build.log
- uses: actions/upload-artifact@v4
with:
name: frpc
path: android/app/src/main/jniLibs/
path: android/app/src/full/jniLibs/
if-no-files-found: error

apk:
Expand All @@ -94,7 +94,8 @@ jobs:
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
# tor-android's classes are Java 24 bytecode
java-version: "25"

- uses: dtolnay/rust-toolchain@stable
with:
Expand Down Expand Up @@ -126,13 +127,14 @@ jobs:
J=android/app/src/main/jniLibs
for abi in arm64-v8a armeabi-v7a x86_64; do
cp tunnels/easytier-$abi/libeasytier.so $J/$abi/
cp tunnels/frpc/$abi/libfrpc.so $J/$abi/
mkdir -p android/app/src/full/jniLibs/$abi
cp tunnels/frpc/$abi/libfrpc.so android/app/src/full/jniLibs/$abi/
done
ls -l $J/*
ls -l $J/* android/app/src/full/jniLibs/*

- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.14.3"
gradle-version: "9.8.2"

- name: Signing key
env:
Expand All @@ -157,18 +159,26 @@ jobs:
run: |
set -o pipefail
gradle -p android --no-daemon assembleRelease 2>&1 | tee build.log
d=android/app/build/outputs/apk/release
d=android/app/build/outputs/apk
ls -R $d
mkdir -p out
cp $d/app-universal-release.apk out/vivant-android.apk
cp $d/app-arm64-v8a-release.apk out/vivant-android-arm64.apk
cp $d/app-armeabi-v7a-release.apk out/vivant-android-armv7.apk
cp $d/app-x86_64-release.apk out/vivant-android-x86_64.apk
for f in libvivant.so libeasytier.so libfrpc.so; do
for abi in arm64-v8a armeabi-v7a x86_64; do
unzip -l out/vivant-android.apk | grep -q "lib/$abi/$f" || { echo "missing $abi/$f"; exit 1; }
done
unzip -l out/vivant-android-arm64.apk | grep -q "lib/arm64-v8a/$f" || { echo "arm64 apk missing $f"; exit 1; }
# full edition (Tor + EasyTier + FRP)
cp $d/full/release/app-full-universal-release.apk out/vivant-android.apk
cp $d/full/release/app-full-arm64-v8a-release.apk out/vivant-android-arm64.apk
cp $d/full/release/app-full-armeabi-v7a-release.apk out/vivant-android-armv7.apk
cp $d/full/release/app-full-x86_64-release.apk out/vivant-android-x86_64.apk
# EasyTier-only edition
cp $d/easytier/release/app-easytier-universal-release.apk out/vivant-android-easytier.apk
cp $d/easytier/release/app-easytier-arm64-v8a-release.apk out/vivant-android-easytier-arm64.apk
cp $d/easytier/release/app-easytier-armeabi-v7a-release.apk out/vivant-android-easytier-armv7.apk
cp $d/easytier/release/app-easytier-x86_64-release.apk out/vivant-android-easytier-x86_64.apk
has() { unzip -l "$1" | grep -q "lib/$2/$3"; }
for abi in arm64-v8a armeabi-v7a x86_64; do
for f in libvivant.so libeasytier.so libfrpc.so libtor.so; do has out/vivant-android.apk $abi $f || { echo "full: missing $abi/$f"; exit 1; }; done
for f in libvivant.so libeasytier.so; do has out/vivant-android-easytier.apk $abi $f || { echo "easytier: missing $abi/$f"; exit 1; }; done
for f in libfrpc.so libtor.so; do ! has out/vivant-android-easytier.apk $abi $f || { echo "easytier edition must not contain $f"; exit 1; }; done
done
has out/vivant-android-easytier-arm64.apk arm64-v8a libeasytier.so
ls -l out

- name: Report failure
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ jobs:

Windows / macOS:`vivant-windows-*.exe`、`vivant-macos-*` 只适合本机测试或局域网,没有安装器、沙箱和开机自启 · for local testing or a LAN only — no installer, sandbox or autostart.

安卓手机当服务器(含 EasyTier / FRP 外网访问)· Android phone as a server (with EasyTier / FRP internet access): 大多数手机下载 `vivant-android-arm64.apk`,不确定就下 `vivant-android.apk`(通用版)· most phones: `vivant-android-arm64.apk`; not sure: `vivant-android.apk` (universal)(Android 8+,见 [docs/android.md](https://github.com/Freecode100Year/vivant/blob/main/docs/android.md) · see [docs/android.en.md](https://github.com/Freecode100Year/vivant/blob/main/docs/android.en.md))
安卓手机当服务器(Tor 洋葱服务 / EasyTier / FRP,不暴露 IP)· Android phone as a server (Tor onion service / EasyTier / FRP, no exposed IPs): 大多数手机下载 `vivant-android-arm64.apk`,不确定就下 `vivant-android.apk`(通用版)· most phones: `vivant-android-arm64.apk`; not sure: `vivant-android.apk` (universal)(Android 8+,见 [docs/android.md](https://github.com/Freecode100Year/vivant/blob/main/docs/android.md) · see [docs/android.en.md](https://github.com/Freecode100Year/vivant/blob/main/docs/android.en.md))

只要 EasyTier 的精简版(不含 Tor / FRP,更小)· EasyTier-only edition (no Tor / FRP, smaller): `vivant-android-easytier-arm64.apk`,通用版 · universal `vivant-android-easytier.apk`

程序 Binaries: `vivant-linux-amd64` (x86_64), `vivant-linux-arm64` (ARM64, e.g. Oracle Ampere), `vivant-linux-armv7`;用 `SHA256SUMS` 校验 · verify with `SHA256SUMS`. (`ptterm-linux-*` are the same files under the old names.)
EOF
Expand Down
4 changes: 2 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Manage it with `sudo vivant-ctl status | upgrade | uninstall`. If people can't c

**Server can't reach GitHub:** download the offline one-click installer from the release page and run `bash vivant-<version>-linux-offline.sh --to root@SERVER` on your computer, see [Offline install](docs/deploy.en.md#offline-install-server-cant-reach-github). For a temporary server on Windows / macOS (testing, LAN), the release page has those binaries too, see [Deployment and configuration](docs/deploy.en.md#windows--macos).

**An Android phone as the server:** install `vivant-android-arm64.apk`, tap "Start", and people on the same Wi-Fi or the phone's hotspot can join; EasyTier or FRP lets people elsewhere in too, see [Android phone as a server](docs/android.en.md).
**An Android phone as the server:** install `vivant-android-arm64.apk`, tap "Start", and people on the same Wi-Fi or the phone's hotspot can join; a Tor onion service lets people elsewhere in without anyone seeing an IP, see [Android phone as a server](docs/android.en.md).

**Upgrading from Private-Talk Terminal:** VIVANT used to be called Private-Talk Terminal (`ptterm`). Run the command above once on the old server and it migrates automatically, keeping the host key, configuration, bans and onion address. See [Deployment and configuration](docs/deploy.en.md#migrating-from-private-talk-terminal).

Expand All @@ -95,7 +95,7 @@ Improvements chosen after studying similar projects (ssh-chat, devzat, SoftRoom,

## Documentation

- [Android phone as a server](docs/android.en.md): installing the APK, hotspot / LAN connections, building it
- [Android phone as a server](docs/android.en.md): installing the APK, Tor onion service (no exposed IPs), hotspot / LAN, building it
- [Connecting and using](docs/usage.en.md): every platform, iPhone step by step, all commands, Tor
- [AI agents](docs/agents.en.md): `bot@` JSON mode, example program
- [Security and anonymity](docs/security.en.md): what the operator can see and what can't be promised
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ curl -fsSL https://raw.githubusercontent.com/Freecode100Year/vivant/main/install

**服务器连不上 GitHub:** 下载发布页里的离线一键安装包,在自己电脑上运行 `bash vivant-<版本>-linux-offline.sh --to root@服务器` 即可部署,见[离线安装](docs/deploy.md#离线安装服务器连不上-github)。想在 Windows / macOS 上临时跑一个(测试、局域网),发布页也有对应的程序,见[部署与配置](docs/deploy.md#windows--macos)。

**用安卓手机当服务器:** 装上 `vivant-android-arm64.apk`,点一下「启动」,同一 Wi-Fi 或手机热点下的人就能连进来;还能用 EasyTier 或 FRP 让外网的人进来,见[安卓手机当服务器](docs/android.md)。
**用安卓手机当服务器:** 装上 `vivant-android-arm64.apk`,点一下「启动」,同一 Wi-Fi 或手机热点下的人就能连进来;还能通过 Tor 洋葱服务让外网的人进来,谁都看不到 IP,见[安卓手机当服务器](docs/android.md)。

**从 Private-Talk Terminal 升级:** VIVANT 以前叫 Private-Talk Terminal(`ptterm`)。在旧服务器上运行一次上面的命令即可自动迁移,主机密钥、配置、封禁、洋葱地址都会保留,详见[部署与配置](docs/deploy.md#从-private-talk-terminal-迁移)。

Expand Down Expand Up @@ -98,7 +98,7 @@ curl -fsSL https://raw.githubusercontent.com/Freecode100Year/vivant/main/install
- [连接与使用](docs/usage.md):各平台连接方法、iPhone 详细步骤、全部命令、Tor
- [AI 智能体接入](docs/agents.md):`bot@` JSON 模式、示例程序
- [安全与匿名性](docs/security.md):部署者能看到什么、不能保证什么
- [安卓手机当服务器](docs/android.md):APK 安装、热点 / 局域网连接、自己构建
- [安卓手机当服务器](docs/android.md):APK 安装、Tor 洋葱服务(不暴露 IP)、热点 / 局域网、自己构建
- [部署与配置](docs/deploy.md):一键安装参数、迁移、管理员、配置项、Docker
- [性能](docs/performance.md):压测数据和设计
- [开发](docs/development.md):构建、测试、发布、代码结构、路线图
Expand Down
1 change: 1 addition & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ local.properties
*.keystore
# built by build-native.sh
app/src/main/jniLibs/
app/src/full/jniLibs/
.tunnel-src/
75 changes: 0 additions & 75 deletions android/app/build.gradle

This file was deleted.

105 changes: 105 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import com.android.build.api.dsl.ApplicationExtension

plugins {
id("com.android.application")
}

// Version comes from the repository's VERSION file (e.g. 0.11.4), or from
// VIVANT_VERSION (v0.11.4) in CI, so the APK always matches the server.
val rawVersion = (System.getenv("VIVANT_VERSION") ?: rootProject.file("../VERSION").readText())
.trim().removePrefix("v")
val versionNums = Regex("""^(\d+)\.(\d+)\.(\d+)""").find(rawVersion)?.destructured
val code = versionNums?.let { (a, b, c) -> a.toInt() * 10000 + b.toInt() * 100 + c.toInt() } ?: 1

configure<ApplicationExtension> {
namespace = "io.github.freecode100year.vivant"
// tor-android is built against API 37.1
compileSdk {
version = release(37) {
minorApiLevel = 2
}
}

defaultConfig {
applicationId = "io.github.freecode100year.vivant"
minSdk = 26
targetSdk = 35
versionCode = code
versionName = rawVersion
}

// Release signing: set VIVANT_KEYSTORE (path), VIVANT_KEYSTORE_PASSWORD,
// VIVANT_KEY_ALIAS, VIVANT_KEY_PASSWORD. Without them the release APK is
// signed with the debug key (fine for trying it out; updates need the
// same key every time).
val ks = System.getenv("VIVANT_KEYSTORE")
signingConfigs {
create("release") {
if (!ks.isNullOrEmpty()) {
storeFile = file(ks)
storePassword = System.getenv("VIVANT_KEYSTORE_PASSWORD")
keyAlias = System.getenv("VIVANT_KEY_ALIAS")
keyPassword = System.getenv("VIVANT_KEY_PASSWORD") ?: System.getenv("VIVANT_KEYSTORE_PASSWORD")
}
}
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName(if (ks.isNullOrEmpty()) "debug" else "release")
}
}

// Two editions: "full" (Tor onion service, EasyTier, FRP) and "easytier"
// (EasyTier only: smaller, simpler). They install side by side.
flavorDimensions += "edition"
productFlavors {
create("full") {
dimension = "edition"
}
create("easytier") {
dimension = "edition"
applicationIdSuffix = ".easytier"
versionNameSuffix = "-easytier"
}
}

// One APK per CPU type keeps downloads small; the universal one works everywhere.
splits {
abi {
isEnable = true
reset()
include("arm64-v8a", "armeabi-v7a", "x86_64")
isUniversalApk = true
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

// The server and tunnel clients are real executables (lib*.so) started as
// child processes; they must be extracted to nativeLibraryDir, the only
// place an app may execute files from.
packaging {
jniLibs {
useLegacyPackaging = true
excludes += "lib/x86/**" // tor-android ships 32-bit x86 too; nobody needs it
}
}

lint {
abortOnError = false
checkReleaseBuilds = false
}
}

dependencies {
// full edition only: Tor itself (the same build Orbot uses) and its control-port library
add("fullImplementation", "info.guardianproject:tor-android:0.4.9.14")
add("fullImplementation", "info.guardianproject:jtorctl:0.4.5.7")
// Snowflake / obfs4 / webtunnel bridges for censored networks (as in Orbot)
add("fullImplementation", "com.netzarchitekten:IPtProxy:5.7.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.freecode100year.vivant;

import android.content.Context;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/** EasyTier-only edition: no Tor inside. Same API as the full edition's TorOnion, doing nothing. */
final class TorOnion {
static final String BRIDGES_NONE = "none", BRIDGES_SNOWFLAKE = "snowflake", BRIDGES_CUSTOM = "custom";

TorOnion(Context ctx) {}

static File serviceDir(Context c) { return new File(c.getFilesDir(), "onion"); }

static String address(Context c) { return null; }

static List<String> parseBridges(String text) { return new ArrayList<>(); }

void start(int localPort) { throw new UnsupportedOperationException("this edition has no Tor"); }

void stop() {}
}
6 changes: 6 additions & 0 deletions android/app/src/easytier/res/values/edition.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="edition_easytier">true</bool>
<string name="app_name">VIVANT ET</string>
<string name="net_title">外网访问:EasyTier(不记录 IP)· Internet access: EasyTier (no IPs kept)</string>
</resources>
4 changes: 4 additions & 0 deletions android/app/src/full/assets/snowflake-bridges.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Tor Browser's built-in Snowflake bridges (as shipped by Orbot, builtin-bridges.json).
# Tor Browser 内置的 Snowflake 网桥。
snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=app.datapacket.com,www.datapacket.com ice=stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.telnyx.com:3478,stun:stun.hot-chilli.net:3478,stun:stun.fitauto.ru:3478,stun:stun.m-online.net:3478 utls-imitate=hellorandomizedalpn
snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://1098762253.rsc.cdn77.org/ fronts=app.datapacket.com,www.datapacket.com ice=stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.telnyx.com:3478,stun:stun.hot-chilli.net:3478,stun:stun.fitauto.ru:3478,stun:stun.m-online.net:3478 utls-imitate=hellorandomizedalpn
Loading
Loading