# 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.nextcli.com/cloud/app-with-vpn.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
