# APP VPN SDK\_让APP自带VPN/防域名屏蔽/防DNS污染

{% hint style="success" %}
**App 应用打包使用说明**

本服务可将任何网站 URL 打包为 Android 与 Windows 应用（iOS、Mac 版本正在开发中），并在应用内部集成了加密隧道访问（防域名屏蔽、防DNS污染）功能，使海内外用户均可自由访问目标网站/接口API，不受防火墙/网络异常等特殊突发情况的限制。

同时，您也可以在此生成并下载专用的 Android 与 Windows 防屏蔽 SDK，将其整合至您的 App，以便突破网络访问限制。以下示例以 X(推特) 为例，展示如何使用本功能。
{% endhint %}

### 一、APP打包，SDK集成 <a href="#app-package" id="app-package"></a>

#### 1.1 创建APP <https://cloud.nextcli.com/app/myapp>  <a href="#create-app" id="create-app"></a>

<table><thead><tr><th width="173">字段</th><th>说明</th></tr></thead><tbody><tr><td>APP包名/APP ID</td><td><a data-footnote-ref href="#user-content-fn-1">包名</a>? 是APP的唯一ID，通常以 com.开头，如: <code>com.x.app</code> <br>不建议使用主流APP的名字，包名冲突，会覆盖已安装的同包名ID的APP；</td></tr><tr><td>APP适用系统</td><td>普通用户可选择打包Android APK、Windows EXE；<br>安卓AAR、Windows DLL 则是面向专业开发者的SDK。</td></tr><tr><td>APP主页链接</td><td>打开APP时，主页显示内容所在的链接网址；</td></tr><tr><td>APP图标</td><td>APP安装在桌面上显示的图标；<br>Android推荐使用 512×512px 的图标以获得最佳效果；<br>Windows同样512×512px 的图标，也可以直接使用网站的favicon.ico链接。</td></tr></tbody></table>

#### 1.2. 打包与下载 <a href="#package-and-download" id="package-and-download"></a>

* 点击“打包”后，预计 1～5 分钟完成。需手动刷新查看进度。
* 打包产物下载链接有效期为 1小时，过期后需重新打包。
* 如果打包失败，请检查错误提示；如无法解决可联系客服。

#### 1.3. 其他说明 <a href="#other-notes" id="other-notes"></a>

* 新用户在创建第一个应用后，可获得 1年 的每月 1G 流量套餐。

### 二、流量控制 <https://cloud.nextcli.com/app/whitelist>  <a href="#whitelist" id="whitelist"></a>

#### 2.1 流量加速 <a href="#traffic-speed-up" id="traffic-speed-up"></a>

* 开启流量加速后，白名单中的域名将启用防屏蔽功能。
* 如果当月流量用完，需要再次购买套餐或流量包才能继续使用流量加速。

#### **2.2 什么是加速白名单？** <a href="#what-is-whitelist" id="what-is-whitelist"></a>

* 流量加速开启后，白名单中的域名会通过加速通道，消耗套餐流量；其他域名则使用普通网络。
* 创建应用后，系统会自动将应用主页链接的域名加入到加速白名单。

  例如，若应用主页链接为 abc.x.com，则默认将 abc.x.com 添加到白名单。
* 您还可以手动添加更多域名到白名单，例如添加 123.x.com 、[www.google.com，则该域名流量将走加速通道。](http://www.google.com，则该域名流量将走加速通道。)

#### **2.3 白名单规则** <a href="#whitelist-rules" id="whitelist-rules"></a>

1. 仅可添加域名（不能包含 http 或 https）。
2. 若添加一级域名（如 x.com），其所有二级或子域名（如 m.x.com, [www.x.com）皆自动纳入白名单。](http://www.x.com）皆自动纳入白名单。)
3. 若只添加二级域名（如 m.x.com），则不包含 x.com。

### 三、流量套餐 <https://cloud.nextcli.com/app/plugin>  <a href="#data-packages" id="data-packages"></a>

* 新用户创建第一个应用后，系统会免费提供 1年 的每月 1G 流量套餐。
* 您可多次购买同一套餐，后购买的套餐将覆盖之前的套餐，同时重置 App 的流量用量和到期时间。
* ⚠️注意，必须先拥有一个套餐，才能购买流量包。流量包的有效期与当月套餐同步。
* 如需更多套餐选项，请联系客服。

### 四、开发者文档 <a href="#developer-docs" id="developer-docs"></a>

#### 4.1  生成AAR <a href="#generate-aar" id="generate-aar"></a>

1. 在创建应用时选择 AAR 作为目标产物，主页链接可先填后端 API 地址（后续可于白名单中修改）。
2. 打包产物解压后将获得 checksum 文件与 xxx.aar 文件。
3. sdk调用前需要把checksum复制到app的内部存储文件夹下，因为sdk需要校验这个文件，然后初始化sdk，参考以下demo

{% tabs %}
{% tab title="kotlin" %}

<pre class="language-kotlin"><code class="lang-kotlin"><strong>// kotlin 示例
</strong><strong>CoroutineScope(Dispatchers.IO).launch {
</strong>    try {
        moveAssetsFileToInternalStorage(context, "checksum")

        // code=1 正常启动
        // 这里sdk工作目录必须和checksum文件在同一个目录
        val errCode = Safetunnel.newInstance(context.filesDir.absolutePath)

        // code=1 正常启动
        val startCode = Safetunnel.start()

        // 获取代理端口，应用层流量转发到这个端口即可使用流量加速功能
        val port = Safetunnel.getPort()
    } catch (e: Exception) {
        // log
    }

// Android复制assets文件到内部存储
private suspend fun moveAssetsFileToInternalStorage(
    context: Context,
    fileName: String
): Boolean {
    return withContext(Dispatchers.IO) {
        var inputStream: InputStream? = null
        try {
            inputStream = context.assets.open(fileName)
            // ⚠️注意：sdk工作目录必须和checksum文件在同一个目录
            val outFile = File(context.filesDir, fileName).apply {
                parentFile?.mkdirs()
            }
            if (!outFile.exists()) {
                // 将文件内容从输入流复制到输出流
                inputStream.copyTo(outFile.outputStream())
                Log.d(
                    "TAG",
                    "File $fileName moved to internal storage: ${outFile.absolutePath}"
                )
            }
            true
        } catch (e: Exception) {
            Log.e("TAG", "Failed to move file $fileName to internal storage: ${e.message}")
            false
        } finally {
            // 关闭流
            inputStream?.close()
        }
    }
}
</code></pre>

{% endtab %}

{% tab title="java" %}

```java
  // java 示例
  new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            // 将 assets 文件移动到内部存储
            boolean isMoved = moveAssetsFileToInternalStorage(context, "checksum");
            if (!isMoved) {
                Log.e(TAG, "Failed to move checksum file.");
                return;
            }

            // ⚠️注意：SDK 工作目录必须和 checksum 文件在同一个目录
            int errCode = Safetunnel.newInstance(context.getFilesDir().getAbsolutePath());
            if (errCode != 1) {
                Log.e(TAG, "Safetunnel.newInstance failed with code: " + errCode);
                return;
            }

            // 启动 Safetunnel
            int startCode = Safetunnel.start();
            if (startCode != 1) {
                Log.e(TAG, "Safetunnel.start failed with code: " + startCode);
                return;
            }

            // 获取代理端口，应用层流量转发到这个端口即可使用流量加速功能
            int port = Safetunnel.getPort();
            Log.d(TAG, "Proxy port: " + port);

        } catch (Exception e) {
            Log.e(TAG, "Exception occurred: " + e.getMessage());
        }
    }
}).start();

// Android复制assets文件到内部存储，java示例
public static boolean moveAssetsFileToInternalStorage(Context context, String fileName) {
    InputStream inputStream = null;
    try {
        inputStream = context.getAssets().open(fileName);
        // 注意：SDK工作目录必须和checksum文件在同一个目录
        File outFile = new File(context.getFilesDir(), fileName);
        File parentDir = outFile.getParentFile();
        if (parentDir != null && !parentDir.exists()) {
            parentDir.mkdirs();
        }
        if (!outFile.exists()) {
            // 将文件内容从输入流复制到输出流
            try (FileOutputStream outputStream = new FileOutputStream(outFile)) {
                byte[] buffer = new byte[1024];
                int length;
                while ((length = inputStream.read(buffer)) > 0) {
                    outputStream.write(buffer, 0, length);
                }
            }
            Log.d(TAG, "File " + fileName + " moved to internal storage: " + outFile.getAbsolutePath());
        }
        return true;
    } catch (Exception e) {
        Log.e(TAG, "Failed to move file " + fileName + " to internal storage: " + e.getMessage());
        return false;
    } finally {
        // 关闭输入流
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (Exception e) {
                Log.e(TAG, "Failed to close input stream: " + e.getMessage());
            }
        }
    }
}
```

{% endtab %}

{% tab title="C#" %}

```csharp
// C#复制assets文件到内部存储
private string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "com.safelink.test", "Cache");

// 导入dll
[DllImport("libcore.dll")]
private static extern int NewInstance(string dir);

[DllImport("libcore.dll")]
private static extern int Start();

[DllImport("libcore.dll")]
private static extern int GetPort();

 private async void initSdk()
  {
      String filePath = Path.Combine(cachePath, "file");
      if (!Directory.Exists(filePath))
      {
          Directory.CreateDirectory(filePath);
      }
      if (!File.Exists(Path.Combine(filePath, "checksum")))
      {
          ExtractResourceToFile("pack://application:,,,/tmp/checksum", Path.Combine(filePath, "checksum"));
      }
      try
      {
          var ErrCode = NewInstance(filePath);
          Debug.WriteLine("NewInstance ErrCode:"+ ErrCode);
          if (ErrCode == 1)
          {
              ErrCode=Start();
              Debug.WriteLine("Start ErrCode:" + ErrCode);
          }
      }
      catch (Exception ex)
      {
          Debug.WriteLine("initSdk error:" + ex.ToString());
      }
  }

// 复制文件到内部存储
public static void ExtractResourceToFile(string resourceName, string filepath)
{
    try
    {
        // 获取当前程序集

        StreamResourceInfo info = Application.GetContentStream(new Uri(resourceName));

        // 使用资源名称从程序集获取资源流
        using (Stream resourceStream = info.Stream)
        {
            if (resourceStream == null)
            {
                throw new InvalidOperationException("Could not find resource: " + resourceName);
            }

            // 创建目标文件路径的目录
            Directory.CreateDirectory(Path.GetDirectoryName(filepath));

            // 创建文件流并写入从程序集提取的数据
            using (FileStream fileStream = new FileStream(filepath, FileMode.Create, FileAccess.Write))
            {
                resourceStream.CopyTo(fileStream);
            }
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show("Error extracting resource: " + ex.Message);
    }
}
```

{% endtab %}
{% endtabs %}

4. 应用层示例，这里以网络请求为例，更多使用示例请查看demo；

{% tabs %}
{% tab title="Android " %}

```kotlin
# Android kotlin 示例
val proxy =
    Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", Safetunnel.getPort()))
okHttpClient.proxy(proxy)
```

{% endtab %}

{% tab title="Windows" %}

```csharp
# Windows C# 示例
// 创建一个 HttpClientHandler，并设置代理信息
var httpClientHandler = new HttpClientHandler
{
    Proxy = new WebProxy("127.0.0.1:"+GetPort()),
    UseProxy = true,
};

// 使用 HttpClientHandler 创建 HttpClient
using (var httpClient = new HttpClient(httpClientHandler))
{
    // 发送 HTTP GET 请求
    HttpResponseMessage response = await httpClient.GetAsync("http://example.com");

    if (response.IsSuccessStatusCode)
    {
        // 处理成功响应
        string content = await response.Content.ReadAsStringAsync();
        Console.WriteLine(content);
    }
    else
    {
        // 处理错误响应
        Console.WriteLine($"HTTP Error: {response.StatusCode}");
    }
}
```

{% endtab %}
{% endtabs %}

### 五、错误码 <a href="#code" id="code"></a>

<table><thead><tr><th width="143.6328125">code</th><th>备注</th></tr></thead><tbody><tr><td>1</td><td>成功</td></tr><tr><td>101</td><td>包名错误、不匹配</td></tr><tr><td>103</td><td>checksum文件不存在(默认的域名白名单文件)</td></tr><tr><td>104</td><td>checksum文件解密失败</td></tr><tr><td>105</td><td>下载配置文件失败</td></tr><tr><td>106、206</td><td>解密配置文件失败</td></tr><tr><td>107、207</td><td>json解析配置文件失败</td></tr><tr><td>108</td><td>未找到可用tag</td></tr><tr><td>109</td><td>已开启</td></tr><tr><td>111</td><td>已经close</td></tr><tr><td>110</td><td>start失败</td></tr><tr><td>112</td><td>端口开启错误</td></tr><tr><td>208</td><td>yaml解析失败</td></tr><tr><td>209</td><td>Options对象序列化为JSON 失败</td></tr><tr><td>210</td><td>加密JSON失败</td></tr><tr><td>211</td><td>创建box实例失败</td></tr></tbody></table>

比如：105 下载配置文件失败，可能是因为请求IP在国外，我们的配置文件在国内，国内不会异常！

更多问题请[联系客服 @NextCLiCloudBOT >>](https://t.me/NextCLiCloudBOT?start\&text=%23APP%E6%89%93%E5%8C%85%20%E9%94%99%E8%AF%AF%E7%A0%81%0A)

***

#### 参考链接

• [Android 开发者文档（英文）](https://developer.android.com/docs)

• [Windows 应用打包（Microsoft 官方文档）](https://learn.microsoft.com/en-us/windows/apps/)

[^1]: 1、什么是应用包名：

    <https://dev.mi.com/docs/appsmarket/operation_docs/package_name/>

    2、安卓官方对 Application ID的说明：

    <https://developer.android.com/build/configure-app-module?hl=zh-cn#set-application-id>
