LinkLoomLinkLoom
首页
指南
功能特性
SDK
API
GitHub
首页
指南
功能特性
SDK
API
GitHub
  • VPN SDK

    • LinkLoom VPN SDK
    • LinkLoom VPN SDK 集成指南

LinkLoom VPN SDK 集成指南

把"VPN 初始化 + UDP 隧道 + 设备注册"封装为 AAR,让任意 Android 项目通过 Builder API 接入虚拟内网。


目录

  1. SDK 能力概览
  2. 环境要求
  3. 集成步骤
  4. 完整示例代码
  5. API 参考
  6. UDP 协议规范
  7. 混淆规则
  8. 常见问题
  9. 从 linkloom2 迁移

1. SDK 能力概览

数据流

[宿主应用业务流量]
       │
       ▼  被系统 TUN 接口捕获
[TUN 接口] ──read──► forwardToUdp() ──► SimpleUdpTunnel.send()
                                              │
                                              ▼
                                  [UDP 中继服务器 host:port]
                                              │
                                              ▼
[宿主应用] ◄──write── VPN-Write 线程 ◄── BlockingQueue ◄── PacketHandler ◄── UDP-Receive 线程

内置功能

  • UDP 配置后端拉取(GET /udp-config):服务器 host/port、MTU、socket buffer 全部由后端下发,Builder 里只是兜底默认值
  • 设备注册(GET /device/validate/{id} + POST /device/bind),不返回 networkKey
  • UDP 隧道:注册请求、心跳保活(默认 30s 间隔 / 90s 超时)、原生 IP 包透传
  • 数据面双工转发,带 IPv4 包校验(version==4 && ihl>=5 && length>=20)
  • TUN 接口建立与读写(两个独立线程 + BlockingQueue 解耦,TUN 流先于线程启动,避免竞态)
  • 三路 token 并投:请求头 satoken + GET query ?apiKey= + POST body "apiKey":"..."(参数名均可配)
  • 前台服务通知(自定义图标/渠道名/点击跳转)
  • WakeLock 保持 CPU
  • JobScheduler 守护(默认 60s 巡检,可关闭)
  • 自动处理 VpnService.prepare 授权(透明 VpnPermissionActivity)
  • excludeSelf 路由开关(默认 false,宿主流量走 VPN;SDK 自动 VpnService.protect() UDP socket 避免死循环;可选 true 让宿主绕过 VPN)

不会做的事

  • ❌ 不处理用户登录/Token 持久化(由 TokenProvider 注入当前 token)
  • ❌ 不做错误页跳转(错误经 VpnStateListener.onError 上抛,调用方决定 UI)
  • ❌ 不强制路由模式(默认仅路由配置的网段,如 10.57.0.0/24,不会全局代理)

2. 环境要求

项要求
minSdk28 (Android 9.0)
targetSdk / compileSdk34
Java 源码版本11
Gradle 插件AGP 9.0.1 / Gradle 9.1.0(向下兼容到 AGP 8.x)
传递依赖okhttp3:okhttp:4.12.0、com.google.code.gson:gson:2.10.1、androidx.core:core

如果宿主项目已经依赖了不同版本的 OkHttp/Gson,AAR 会复用宿主版本。建议宿主使用 4.x 的 OkHttp 与 2.10+ 的 Gson。


3. 集成步骤

3.1 构建 AAR

cd linkloom-vpn-sdk
# Windows
gradlew.bat :vpnsdk:assembleRelease
# macOS / Linux
./gradlew :vpnsdk:assembleRelease

产物路径:vpnsdk/build/outputs/aar/vpnsdk-release.aar

3.2 把 AAR 放入宿主工程

把 vpnsdk-release.aar 拷贝到宿主 app/libs/ 目录。

3.3 添加 gradle 依赖

宿主 app/build.gradle:

dependencies {
    // 本地 AAR
    implementation files("libs/vpnsdk-release.aar")

    // 传递依赖(AAR 不携带版本,宿主必须显式声明)
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
    implementation 'com.google.code.gson:gson:2.10.1'

    // 如果宿主没有 androidx.core,需要补
    implementation 'androidx.core:core:1.13.1'
}

为什么 AAR 不带依赖版本? Android Library 的 pom 默认会带传递依赖版本,但本地 AAR 不带 pom,因此需要在宿主显式声明。这样能避免版本冲突,宿主可以自由升级。

3.4 AndroidManifest 权限

SDK 已通过 manifest merger 自动声明下列权限,通常无需任何额外配置:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

Service / Activity 也由 SDK manifest 自动注册:

<service android:name="com.linkloom.vpnsdk.core.LinkLoomVpnService" ... />
<service android:name="com.linkloom.vpnsdk.core.VpnWatchdogJobService" ... />
<activity android:name="com.linkloom.vpnsdk.core.VpnPermissionActivity" ... />

3.5 Android 13+ 通知运行时权限

targetSdk ≥ 33 时,前台服务通知 需要运行时申请 POST_NOTIFICATIONS。建议在 Application.onCreate 或首个 Activity:

if (Build.VERSION.SDK_INT >= 33) {
    registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
        // 即使拒绝,VPN 仍能工作(只是通知不可见)
    }).launch(Manifest.permission.POST_NOTIFICATIONS);
}

3.6 初始化 SDK

在 Application.onCreate(或首个 Activity):

public class MyApp extends Application {
    private static final String TAG = "MyApp";

    @Override
    public void onCreate() {
        super.onCreate();

        LinkLoomVpn vpn = new LinkLoomVpn.Builder(this)
                .apiBaseUrl("http://117.72.215.142:4981")  // 必填(同时用于 /udp-config 拉取)
                .tokenProvider(() -> SessionManager.getToken())  // 必填
                .tokenHeader("satoken")                    // 可选,默认 "satoken"
                .apiKeyParamName("apiKey")                 // 可选,默认 "apiKey";传 null 仅走 header
                .route("10.57.0.0", 24)                    // 可选,默认 10.57.0.0/24
                .heartbeatInterval(30)                     // 可选
                .heartbeatTimeout(90)                      // 可选
                .notificationIcon(R.mipmap.ic_launcher)    // 可选
                .launchActivity(MainActivity.class)        // 可选
                .enableWatchdog(true)                      // 可选,默认 true
                .listener(new VpnStateListener() {
                    @Override public void onStateChanged(@NonNull VpnState s) {
                        Log.i(TAG, "VPN state: " + s);
                    }
                    @Override public void onError(@NonNull VpnErrorCode code, String msg) {
                        Log.e(TAG, "VPN error: " + code + " / " + msg);
                    }
                    @Override public void onAssignedIp(@NonNull String ip) {
                        Log.i(TAG, "VPN assigned IP: " + ip);
                    }
                })
                .build();   // 注册全局单例
    }
}

认证说明:

  • tokenProvider 是 lambda,每次 REST 请求前才求值,登录态变化后 SDK 下次请求自动拿到新 token。
  • 默认行为(对齐参考 SaTokenHttpUtil):token 会同时放到 请求头 satoken、GET query ?apiKey=...、POST body "apiKey":"..." 三处。
  • 后端用标准 Authorization 头时:.tokenHeader("Authorization"),并在 TokenProvider 里返回 "Bearer xxx"。
  • 只想走 header 时:.apiKeyParamName(null),禁用 query/body 注入。

3.7 启动 / 停止

在任意有 UI 上下文的地方:

// 1. 先通过网络选择拿到 networkKey(来自 GET /network/my 列表)
String networkKey = selectedNetwork.getNetworkKey();

// 2a. 便捷:start 同时传 networkKey
LinkLoomVpn.instance().start(context, networkKey);

// —或—
// 2b. 先 setNetworkKey 再 start
LinkLoomVpn.instance().setNetworkKey(networkKey);
LinkLoomVpn.instance().start(context);

// 停止
LinkLoomVpn.instance().stop(context);

// 查询
VpnState state = LinkLoomVpn.instance().getState();
String ip = LinkLoomVpn.instance().getAssignedIp();

注意:未提供 networkKey 就调 start(context),SDK 会立即回调 onError(NO_NETWORK_KEY, ...),不会拉起 VPN 授权对话框。networkKey 属于 Network 实体,由宿主 App 自行从 /network/my 拉取后让用户选择。


4. 完整示例代码

4.1 最小集成(Application 初始化)

MyApp.java:

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        new LinkLoomVpn.Builder(this)
                .server("117.72.215.142", 5060)
                .apiBaseUrl("http://117.72.215.142:4981")
                .tokenProvider(() -> Session.getToken())
                .launchActivity(MainActivity.class)
                .listener(new VpnStateListener() {
                    @Override public void onStateChanged(@NonNull VpnState s) {
                        Toast.makeText(MyApp.this, "VPN: " + s, Toast.LENGTH_SHORT).show();
                    }
                    @Override public void onError(@NonNull VpnErrorCode c, String m) {
                        Toast.makeText(MyApp.this, "VPN Error: " + c, Toast.LENGTH_LONG).show();
                    }
                    @Override public void onAssignedIp(@NonNull String ip) {
                        Toast.makeText(MyApp.this, "VPN IP: " + ip, Toast.LENGTH_SHORT).show();
                    }
                })
                .build();
    }
}

AndroidManifest.xml:

<application android:name=".MyApp" ...>
    <!-- SDK 内的 Service / Activity 自动合并 -->
</application>

4.2 UI 触发启停

public class MainActivity extends AppCompatActivity {

    public void onConnectClick(View v) {
        LinkLoomVpn.instance().start(this);
    }

    public void onDisconnectClick(View v) {
        LinkLoomVpn.instance().stop(this);
    }
}

4.3 动态注册监听器(在 Activity 中)

private final VpnStateListener stateListener = new VpnStateListener() { /* ... */ };

@Override protected void onStart() {
    super.onStart();
    LinkLoomVpn.instance().addListener(stateListener);
}

@Override protected void onStop() {
    super.onStop();
    LinkLoomVpn.instance().removeListener(stateListener);
}

5. API 参考

5.1 LinkLoomVpn

方法说明
Builder(context)构造 Builder
LinkLoomVpn.instance()取得 build 后的全局单例
start(context)启动 VPN(异步)
start(context, networkKey)设置 networkKey 并启动(便捷重载)
setNetworkKey(networkKey)设置当前选中的网络密钥(start 前必须设置)
stop(context)停止 VPN(异步)
getState()当前 VpnState
getAssignedIp()服务器分配的内网 IP(连接前为 null)
addListener(l) / removeListener(l)注册 / 移除状态监听器
getConfig()调试用:当前 SdkConfig

networkKey 来源:属于服务端 Network 实体(GET /network/my 列表里的字段),由宿主 App 让用户选择网络后把 networkKey 传给 SDK。SDK 不会从设备接口提取它。

5.2 Builder

必填(缺一会触发 onError(INVALID_CONFIG, ...)):

  • apiBaseUrl(url) — REST base URL(同时用于设备注册和 /udp-config 拉取)
  • tokenProvider(provider) — Token 提供者(必填,决定认证方式)

UDP 服务器:

  • server(host, port) — 可选兜底。UDP 服务器地址默认由后端 /udp-config 下发,此处仅作可选兜底;不调用时若 /udp-config 拉取失败会触发 onError(INVALID_CONFIG, ...)。

认证(Token / apiKey 注入):

  • tokenHeader(name) — token 请求头名称,默认 "satoken"(Sa-Token 框架默认)。后端用标准 Authorization 头时传 "Authorization",并在 TokenProvider 里返回 "Bearer xxx" 形式的值。
  • apiKeyParamName(name) — apiKey 在 GET query / POST body 中的参数名,默认 "apiKey"。传 null 或空串可禁用 query/body 注入(仅走 header)。

三路并投(默认行为,对齐参考 SaTokenHttpUtil):SDK 在每次 REST 请求时同时把 token 放到三处:

  1. 请求头:satoken: <token>
  2. GET query:自动追加 ?apiKey=<token>
  3. POST JSON body:自动注入 "apiKey":"<token>"

仅当 apiKeyParamName 为 null/空时才退化为「只走 header」。

网络/隧道:

  • route(address, prefix) — 默认 10.57.0.0/24
  • mtu(int) — 默认 1500(实际值会被 /udp-config 覆盖)
  • heartbeatInterval(seconds) — 默认 30
  • heartbeatTimeout(seconds) — 默认 90
  • httpTimeout(seconds) — 默认 30
  • excludeSelf(boolean) — 默认 false。是否把宿主应用自身排除出 VPN 路由。
    • false(默认):宿主流量也走 VPN。SDK 自动调用 VpnService.protect(udpSocket) 保护 UDP 隧道 socket,避免 UDP 包回环进 TUN 形成死循环。大多数场景用此默认值。
    • true:整个宿主应用(含 WebView / OkHttp 等所有流量)绕过 VPN,走真实网络。等价于在 VpnService.Builder 上调用 addDisallowedApplication(getPackageName())。

UI / 通知:

  • notificationIcon(resId) — 默认 android.R.drawable.stat_sys_download
  • notificationChannelName(name) — 默认 "LinkLoom VPN Service"
  • launchActivity(Class) — 不配则用包默认启动页 Intent
  • sessionName(name) — 默认 "LinkLoom VPN"

守护:

  • enableWatchdog(boolean) — 默认 true
  • watchdogInterval(ms) — 默认 60000

调试:

  • logEnabled(boolean) — 默认 true
  • listener(VpnStateListener) — 等价 build 后 addListener

5.3 VpnState

值含义
IDLE未启动
CONNECTING设备注册 / UDP 注册 / 等待 IP / 建立 TUN
CONNECTED已连接,正在转发
DISCONNECTED已停止(主动 stop 或守护任务触发的停止)
ERROR异常终止,参见 onError

5.4 VpnErrorCode

值触发场景
INVALID_CONFIG必填项缺失(apiBaseUrl/tokenProvider/route),或 /udp-config 拉取失败且未配置 server() 兜底
VPN_PERMISSION_DENIED用户拒绝 VPN 授权 / 系统撤销(onRevoke)
DEVICE_BOUND_OTHER设备已被其他用户绑定
DEVICE_REGISTER_FAILED/device/bind 调用失败(含 401 token 失效)
NO_NETWORK_KEY宿主 App 未在 start() 前提供 networkKey(应来自网络选择)
UDP_REGISTER_TIMEOUT10s 内未收到服务器分配的 IP
DEVICE_DISABLED收到 0x12,设备未注册或被禁用
ESTABLISH_FAILEDVpnService.establish() 返回 null
NETWORK_ERROR网络/IO 异常
UNKNOWN未分类错误

5.5 TokenProvider

public interface TokenProvider {
    String getToken();   // 返回当前用户的认证 token;未登录时返回 null/空串
}

每次发起 REST 请求前会调用,不需要缓存。

5.6 VpnStateListener

public interface VpnStateListener {
    void onStateChanged(@NonNull VpnState state);              // 主线程
    void onError(@NonNull VpnErrorCode code, String message);  // 主线程
    void onAssignedIp(@NonNull String ip);                     // 主线程,连接成功后触发一次
}

6. UDP 协议规范

此小节定义 SDK 与中继服务器之间的二进制协议。如果你只使用现有服务器,可跳过。

字节序说明:所有多字节整数(如心跳 timestamp)使用 大端序(big-endian / network byte order)。

类型码方向名称格式
0x01C→S注册请求[0x01][idLen:1][id...][keyLen:1][key...]
0x11S→C注册响应(L3)[0x11][ipLen:1][ip...]
0x11S→C注册响应(L2)[0x11][ipLen:1][ip...][maskLen:1][mask...][gwLen:1][gw...][dnsLen:1][dns...]
0x03C→S心跳请求[0x03](单字节,无 payload)
0x13S→C心跳响应[0x13][timestamp:8 BE]
0x04S→C连接通知[0x04]
0x05S→C服务器断开通知[0x05]
0x12S→C设备禁用[0x12]
其他双向原生 IP 包首字节高 4 位 = 4(IPv4)/ 6(IPv6)
  • id 为设备的 UUID;key 为宿主 App 通过网络选择传入的 networkKey(不是设备接口返回)。
  • 注册响应按 payload 长度自动识别 L3/L2:携带 mask/gateway/dns 即 L2,SDK 据此设置 TUN 掩码与 DNS。
  • 心跳:客户端发送单字节 0x03;服务器回 0x13 + 8 字节大端 timestamp。
  • 心跳首次延迟 20s,之后每 30s 一次。
  • 客户端时钟与服务器相差 > 5s 会在日志打印 warning。
  • 客户端主动断开时(SimpleUdpTunnel.close())会向服务器发 0x04 通知。

7. 混淆规则

consumer-rules.pro 会被 AGP 自动应用到宿主,无需手动配置。关键 keep 项:

# SDK 公共 API
-keep class com.linkloom.vpnsdk.LinkLoomVpn { *; }
-keep class com.linkloom.vpnsdk.LinkLoomVpn$Builder { *; }
-keep class com.linkloom.vpnsdk.SdkConfig { *; }
-keep class com.linkloom.vpnsdk.VpnState { *; }
-keep class com.linkloom.vpnsdk.VpnErrorCode { *; }
-keep class com.linkloom.vpnsdk.VpnStateListener { *; }
-keep class com.linkloom.vpnsdk.auth.TokenProvider { *; }

# Gson 反射需要的 model
-keep class com.linkloom.vpnsdk.device.model.** { *; }

# OkHttp / Gson 标准规则
-dontwarn okhttp3.**
-dontwarn okio.**
-keep class com.google.gson.** { *; }

8. 常见问题

Q1:用户拒绝 VPN 授权怎么处理?

会通过 onError(VPN_PERMISSION_DENIED, ...) 上报。建议 UI 显示 "请到系统设置开启 VPN 权限"。

Q2:通知不显示(Android 13+)

targetSdk ≥ 33 必须运行时申请 POST_NOTIFICATIONS。SDK 已经把权限声明在 manifest,运行时申请由宿主负责。

Q3:UDP 不通

排查清单:

  1. UDP 服务器 IP / 端口正确(默认 117.72.215.142:5060)
  2. 防火墙放行 UDP 入站
  3. 客户端能 ping 通服务器(TCP ping 也行,验证基础网络)
  4. 抓包确认是否有 0x01 注册包到达服务器

Q4:日志报 "Clock skew detected"

客户端与服务器时间相差 > 5 秒。仅是 warning,不影响功能;建议开启 NTP 时间同步。

Q5:VPN 一连接就断 / UDP 死循环

SDK 默认(excludeSelf=false)会调用 VpnService.protect(udpSocket) 保护 UDP 隧道 socket 不进入 TUN 接口,避免回环死循环;此时宿主应用流量会走 VPN。

如果你切到 excludeSelf=true(宿主绕过 VPN),SDK 会改用 addDisallowedApplication(getPackageName()) 排除整个宿主,UDP socket 不需要 protect 也安全。

如果宿主是通过 plugin/子进程加载 SDK,子进程的流量不会自动被排除,需要额外处理——目前 SDK 未暴露此能力,可后续扩展。

Q6:JobScheduler 守护耗电

默认 60s 巡检一次,每次只调 getRunningServices 一次,开销很小。如需省电:enableWatchdog(false),但服务被系统杀掉后不会自动拉起。

Q7:服务端返回 401 怎么办?

SdkHttpClient 会抛 IOException("401 Unauthorized"),最终上报为 DEVICE_REGISTER_FAILED。宿主应在 onError 里跳转登录或刷新 token。

Q8:多进程使用

SDK 是进程内单例(VpnController 静态字段)。如果宿主在多进程都需要 VPN,应在主进程初始化,子进程通过 Service / IPC 触发 start()。本 SDK 未做多进程同步。

Q9:怎么自定义通知文案?

通过宿主 res/values/strings.xml 覆盖:

<string name="linkloom_vpn_title">我的应用 VPN</string>
<string name="linkloom_vpn_status_connecting">连接中...</string>
<string name="linkloom_vpn_status_connected">已连接</string>
Prev
LinkLoom VPN SDK