mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-07 00:36:49 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f190d746ef | ||
|
|
635ebd4db0 | ||
|
|
032f0e4c66 | ||
|
|
19c6844d09 | ||
|
|
d937826f6d | ||
|
|
f9361d0a7d | ||
|
|
bbc2457498 | ||
|
|
cbca9a57af | ||
|
|
72ac5a0986 | ||
|
|
e414236900 | ||
|
|
0069bb089b |
@@ -7,9 +7,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
release_tag:
|
release_tag:
|
||||||
description: Existing formal tag to publish, for example v2.1.0
|
description: Existing formal tag to publish, for example v2.1.2
|
||||||
required: true
|
required: true
|
||||||
default: v2.1.0
|
default: v2.1.2
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -146,9 +146,10 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
title="AimiliVPN V$(cut -d. -f1,2 VERSION) 正式版"
|
title="AimiliVPN V$(tr -d '\r\n' < VERSION) 正式版"
|
||||||
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
||||||
gh release upload "${RELEASE_TAG}" dist/* --clobber --repo "${GITHUB_REPOSITORY}"
|
gh release upload "${RELEASE_TAG}" dist/* --clobber --repo "${GITHUB_REPOSITORY}"
|
||||||
gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md
|
gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md
|
||||||
else
|
else
|
||||||
gh release create "${RELEASE_TAG}" dist/* --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md --verify-tag
|
gh release create "${RELEASE_TAG}" dist/* --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md --verify-tag
|
||||||
|
fi
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ name: Update VPNGate mirror
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "*/15 * * * *"
|
# Offset from quarter-hour peaks because GitHub may delay busy schedules.
|
||||||
|
- cron: "7,22,37,52 * * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|||||||
@@ -1,108 +1,148 @@
|
|||||||
# AimiliVPN 🌐
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)
|
# AimiliVPN
|
||||||
[](https://github.com/baoweise-bot/aimili-vpngate/tree/main)
|
|
||||||
[](https://github.com/baoweise-bot/aimili-vpngate/pkgs/container/aimili-vpngate)
|
|
||||||
|
|
||||||
Bilingual: [中文](#中文) | [English](#english)
|
**面向 Linux VPS 的 VPNGate 节点管理与 HTTP / HTTPS / SOCKS5 代理网关**
|
||||||
|
|
||||||
---
|
[](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)
|
||||||
|
[](https://github.com/baoweise-bot/aimili-vpngate/pkgs/container/aimili-vpngate)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
<a name="中文"></a>
|
**简体中文** · [English](docs/README.en.md) · [日本語](docs/README.ja.md) · [한국어](docs/README.ko.md)
|
||||||
## 中文 (Chinese)
|
|
||||||
|
|
||||||
AimiliVPN 是一款基于官方 VPNGate 开放协议的高性能、零依赖 VPN 代理网关。它以纯 Python 标准库编写,内置美观响应式的管理网页,提供智能并发测速、多路由模式、出站代理网关、实时日志等强大功能。
|
[快速安装](#quick-install) · [完整安装](#installation) · [连接使用](#connection) · [VPS 推荐](#vps) · [社区入口](#community) · [法律声明](#legal)
|
||||||
|
|
||||||
---
|
[](https://339936.xyz)
|
||||||
|
[](https://t.me/arestemple)
|
||||||
|
[](https://www.youtube.com/watch?v=s-ATfXR8BpI)
|
||||||
|
|
||||||
### 📌 当前正式版本:V2.1
|
</div>
|
||||||
|
|
||||||
V2.1 是项目启用正式版本标志后的首个稳定版本。仓库、安装器、命令行更新和 Web 更新检测现在全部统一使用 **`main` 主分支正式通道**。
|
<a id="vps"></a>
|
||||||
|
## VPS 推荐
|
||||||
|
|
||||||
#### V2.1 更新进展
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">BandwagonHost 搬瓦工</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="CN2 GIA" src="https://img.shields.io/badge/电信-CN2_GIA-dc2626?style=flat-square">
|
||||||
|
<img alt="联通 9929" src="https://img.shields.io/badge/联通-9929-f97316?style=flat-square">
|
||||||
|
<img alt="移动 CMIN2" src="https://img.shields.io/badge/移动-CMIN2-16a34a?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>主要特点</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>电信 CN2 GIA、联通 9929、移动 CMIN2 等三网极品优化线路。</li>
|
||||||
|
<li>低延迟、高稳定性,跨境链路质量出色。</li>
|
||||||
|
<li>适合 TikTok 直播运营、海外带货和长期出海业务。</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://bandwagonhost.com/aff.php?aff=81790"><img alt="查看 BandwagonHost" src="https://img.shields.io/badge/立即查看-BandwagonHost-dc2626?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">RackNerd</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="4000GB 每月流量" src="https://img.shields.io/badge/每月流量-4000GB-0284c7?style=flat-square">
|
||||||
|
<img alt="大流量" src="https://img.shields.io/badge/优势-大流量-0ea5e9?style=flat-square">
|
||||||
|
<img alt="高性价比" src="https://img.shields.io/badge/价格-高性价比-2563eb?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>主要特点</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>每月 4000GB 大流量,流量余量更充足。</li>
|
||||||
|
<li>价格实惠,流量与配置的性价比突出。</li>
|
||||||
|
<li>部署成本低,适合需要长期稳定运行的服务。</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://my.racknerd.com/aff.php?aff=18708"><img alt="查看 RackNerd" src="https://img.shields.io/badge/立即查看-RackNerd-0284c7?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
- **节点来源容灾**:依次尝试 VPNGate 官方 HTTPS、官方 HTTP、GitHub Pages HTTPS、GitHub Pages HTTP、VPS 本地最近有效快照和仓库内置初始快照。
|
AimiliVPN 使用 Python 标准库管理 VPNGate 节点,提供节点获取与检测、连接切换、Web 管理后台,以及共用一个端口的 HTTP、HTTPS 网站代理和 SOCKS5 代理服务。
|
||||||
- **获取与切换修复**:缩短被 VPNGate 域名封锁的 VPS 等待时间;切换新节点前先完成预检,目标失败时保留当前可用连接。
|
|
||||||
- **节点可视化**:恢复延迟列,优先显示本机实测延迟;没有实测值时显示 VPNGate 官方预估值并明确标注“仅供参考”。
|
|
||||||
- **国家筛选**:支持带国旗和节点数量的实时多选筛选,选择范围保存到本机,并作用于手动更新和后台周期同步。
|
|
||||||
- **节点操作**:恢复单节点“检测”按钮,补齐收藏、检测、连接和断开状态逻辑。
|
|
||||||
- **镜像同步**:GitHub Pages 每 15 分钟同步并校验官方节点快照,官方 API 被屏蔽时自动回退。
|
|
||||||
- **Web 更新检测**:页面顶部显示 `V2.1 正式版`,只检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示正确更新命令。
|
|
||||||
- **正式发布链路**:GitHub 标签或手动重跑会依次执行 Python 兼容测试、四架构 Docker 冒烟测试、GHCR 镜像发布,全部成功后才发布通用源码包与 SHA-256 校验文件。
|
|
||||||
|
|
||||||
#### 系统与架构兼容性
|
| 项目 | 默认值或支持范围 |
|
||||||
|
| --- | --- |
|
||||||
|
| Web 管理后台 | TCP `8787` + 独立安全路径 + 账号密码 |
|
||||||
|
| 本机代理 | `127.0.0.1:7928`,支持 HTTP、HTTPS `CONNECT` 和 SOCKS5 |
|
||||||
|
| 源码部署 | x64、x86、ARM64、ARM32 Linux |
|
||||||
|
| Docker 镜像 | `linux/amd64`、`linux/386`、`linux/arm64`、`linux/arm/v7` |
|
||||||
|
| 更新通道 | GitHub `main` 正式分支 / 最新正式 Release |
|
||||||
|
|
||||||
| 类型 | 正式支持范围 | 安装或镜像标识 |
|
> [!IMPORTANT]
|
||||||
| --- | --- | --- |
|
> **网络可用性提示:** 不同地区、数据中心和网络服务商可能限制 DNS、VPNGate API、GitHub 镜像或 VPN 协议。镜像与本地缓存只能提高节点列表的可用性,不能保证所有机型都能建立连接。部署前请确认所在地法律和 VPS 服务商条款允许使用 VPN/TUN。
|
||||||
| Linux x64 | Intel/AMD 64 位 VPS | 通用 Python 源码 / Docker `linux/amd64` |
|
|
||||||
| Linux x86 | Intel/AMD 32 位系统 | 通用 Python 源码 / Docker `linux/386` |
|
|
||||||
| Linux ARM64 | AArch64、ARMv8 VPS/开发板 | 通用 Python 源码 / Docker `linux/arm64` |
|
|
||||||
| Linux ARM32 | ARMv7 设备 | 通用 Python 源码 / Docker `linux/arm/v7` |
|
|
||||||
| Linux 发行版 | Debian、Ubuntu、CentOS、RHEL、Rocky、AlmaLinux、Fedora、Oracle Linux、Amazon Linux、Alpine | 使用同一正式核心 |
|
|
||||||
| Docker | Linux 主机上的 amd64、386、arm64、arm/v7 | GHCR 多架构镜像 |
|
|
||||||
|
|
||||||
> AimiliVPN 依赖 Linux 的 TUN、OpenVPN、iptables 和策略路由,因此不发布虚假的 Windows/macOS 原生兼容包。Windows 或 macOS 只能作为代理客户端使用,不能直接运行完整网关;Docker Desktop 同样不等同于具备宿主机 TUN 能力的 Linux 服务器。
|
<a id="quick-install"></a>
|
||||||
|
## 快速安装
|
||||||
|
|
||||||
项目由纯 Python 标准库组成,不需要为 CPU 编译不同的 Python 二进制。GitHub Release 只提供一个通用 Linux 源码包;GHCR 才会实际构建并发布四种 CPU 架构的 Docker 镜像。
|
使用 `root` 用户在受支持的 Linux VPS 上执行:
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🌟 VPS 优选推荐:跑 AimiliVPN 更稳更省心
|
|
||||||
[](https://bandwagonhost.com/aff.php?aff=81790)
|
|
||||||
[](https://my.racknerd.com/aff.php?aff=18708)
|
|
||||||
|
|
||||||
| 推荐 | 适合谁 | 亮点 | 入口 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| **BandwagonHost 搬瓦工** | 更看重国内访问质量、延迟和线路上限的用户 | **顶级三网优化线路**,适合对网络体验、跨境访问质量和长期稳定性要求更高的场景 | [立即查看](https://bandwagonhost.com/aff.php?aff=81790) |
|
|
||||||
| **RackNerd** | 想低成本部署、测试、长期挂机的用户 | **每月 6000GB 流量**,价格实惠、配置给得足,适合入门部署和性价比优先的 VPS 需求 | [立即查看](https://my.racknerd.com/aff.php?aff=18708) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📢 官方交流与反馈
|
|
||||||
[](https://t.me/arestemple)
|
|
||||||
[](https://339936.xyz)
|
|
||||||
[](https://www.youtube.com/watch?v=s-ATfXR8BpI)
|
|
||||||
[](mailto:yaohunse7@gmail.com)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🚀 安装与正式版更新
|
|
||||||
|
|
||||||
#### 方法一:从 main 主分支一键安装(推荐)
|
|
||||||
|
|
||||||
在 Linux VPS 上以 root 用户执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
部署完成后,终端会输出管理网页专属链接。输入 `ml update` 时只会获取并切换到 `origin/main`,不会检测或切换任何测试分支。
|
安装完成后,终端会显示 Web 后台完整地址、随机安全路径、登录账号和密码。输入 `ml` 可打开管理菜单。
|
||||||
|
|
||||||
#### 方法二:GitHub 正式发行包
|
> [!TIP]
|
||||||
|
> 安装前请在 VPS 控制面板启用 TUN/TAP,并确认 `/dev/net/tun` 存在。Web 默认使用 TCP `8787`,安全组建议只允许自己的 IP 访问。
|
||||||
|
|
||||||
[Releases 页面](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)提供以下文件:
|
<a id="installation"></a>
|
||||||
|
## 完整安装
|
||||||
|
|
||||||
- `aimilivpn-v2.1.0-linux-source.tar.gz`:适用于支持 Python 3 和项目系统依赖的 Linux x64、x86、ARM64、ARMv7 主机。
|
### 运行条件
|
||||||
- `sha256sums.txt`:源码包的 SHA-256 校验值。
|
|
||||||
|
|
||||||
#### 方法三:Docker / Docker Compose
|
- 操作系统:Ubuntu、Debian、Alpine、CentOS、RHEL、Rocky Linux、AlmaLinux、Fedora、Oracle Linux 或 Amazon Linux。
|
||||||
|
- 权限与组件:`root`、OpenVPN、iptables、策略路由和 TUN/TAP。
|
||||||
|
- Windows 与 macOS 可作为代理客户端,但不能直接运行完整网关;Docker Desktop 也不等同于具备宿主机 TUN 能力的 Linux VPS。
|
||||||
|
|
||||||
Docker 镜像地址:`ghcr.io/baoweise-bot/aimili-vpngate:2.1`。仓库中的 [`compose.yaml`](./compose.yaml) 已配置主机网络、`NET_ADMIN` 和 TUN 设备:
|
### 方式一:一键源码安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
安装器会部署到 `/opt/aimilivpn` 并注册系统服务。常用命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ml # 打开管理菜单
|
||||||
|
ml status # 查看状态、Web 地址和账号
|
||||||
|
ml logs # 查看实时日志
|
||||||
|
ml restart # 重启服务
|
||||||
|
ml password # 重设 Web 账号密码
|
||||||
|
ml update # 从 main 正式分支更新
|
||||||
|
ml uninstall # 卸载
|
||||||
|
```
|
||||||
|
|
||||||
|
需要先审查脚本时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
sudo bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
通用 Linux 源码包与 SHA-256 校验文件可在 [GitHub Releases](https://github.com/baoweise-bot/aimili-vpngate/releases/latest) 下载,版本变更记录也统一放在 Release Notes 中。
|
||||||
|
|
||||||
|
### 方式二:Docker Compose
|
||||||
|
|
||||||
|
Docker 主机需要 `/dev/net/tun`、host 网络以及 `NET_ADMIN`、`NET_RAW` 权限。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
docker logs -f aimilivpn
|
docker logs -f aimilivpn
|
||||||
```
|
```
|
||||||
|
|
||||||
无法访问 GHCR 或需要自行审查构建过程时,也可以在 VPS 的仓库目录本地构建:
|
正式镜像:`ghcr.io/baoweise-bot/aimili-vpngate:2.1`
|
||||||
|
|
||||||
|
更新容器:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose build
|
docker compose pull
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以直接运行:
|
<details>
|
||||||
|
<summary><strong>查看 docker run 命令</strong></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
@@ -112,229 +152,138 @@ docker run -d \
|
|||||||
--cap-add NET_ADMIN \
|
--cap-add NET_ADMIN \
|
||||||
--cap-add NET_RAW \
|
--cap-add NET_RAW \
|
||||||
--device /dev/net/tun:/dev/net/tun \
|
--device /dev/net/tun:/dev/net/tun \
|
||||||
|
-e UI_HOST=0.0.0.0 \
|
||||||
|
-e UI_PORT=8787 \
|
||||||
|
-e LOCAL_PROXY_HOST=127.0.0.1 \
|
||||||
|
-e LOCAL_PROXY_PORT=7928 \
|
||||||
-v aimilivpn-data:/data \
|
-v aimilivpn-data:/data \
|
||||||
ghcr.io/baoweise-bot/aimili-vpngate:2.1
|
ghcr.io/baoweise-bot/aimili-vpngate:2.1
|
||||||
```
|
```
|
||||||
|
|
||||||
> Docker 方式只支持具备 `/dev/net/tun` 的 Linux 主机,并需要 `NET_ADMIN`、`NET_RAW` 能力。管理页面默认端口为 `8787`,本机 HTTP/SOCKS5 代理默认端口为 `7928`。容器检测到新版本后会提示重新拉取镜像,不会在容器内执行 `git pull`。
|
</details>
|
||||||
|
|
||||||
---
|
<details>
|
||||||
|
<summary><strong>无法拉取 GHCR 时在 VPS 本地构建</strong></summary>
|
||||||
### 💡 快速使用指南 (小白必看)
|
|
||||||
|
|
||||||
部署成功后,如何使用它进行科学上网?
|
|
||||||
|
|
||||||
#### 第一步:登录 Web 管理后台
|
|
||||||
打开浏览器,访问部署完成时提示的专属后台地址(含安全后缀),即可进入精美的暗黑玻璃拟物风管理界面。
|
|
||||||
|
|
||||||
#### 第二步:获取并连接节点
|
|
||||||
1. 首次进入后台,节点列表可能正在进行首次自动测速与拉取。
|
|
||||||
2. 点击 **“更新节点”** 按钮(或通过网页下方的网关/日志进行状态检查),程序会在后台通过多线程并发测速,自动筛选出延迟最低、可连接的 VPNGate 节点。
|
|
||||||
3. 选择您喜欢的出站路由模式:
|
|
||||||
- **智能自动配置**(推荐):如果当前连接的节点失效,系统会在数秒内自动漂移连接至其他备用健康节点,无需手动干预。
|
|
||||||
- **固定国家地区**:只选择指定国家(如日本 JP、韩国 KR、美国 US)的最佳节点。
|
|
||||||
- **固定 IP 节点**:始终锁定连接到这一个特定节点。
|
|
||||||
|
|
||||||
#### 第三步:使用本机代理 (核心步骤)
|
|
||||||
为了防止代理端口暴露至公网被恶意扫描和滥用,AimiliVPN 的双效代理服务(默认端口 **`7928`**,自适应支持 SOCKS5 和 HTTP 协议)**默认仅绑定在本地回环地址(`127.0.0.1`)**,只接收 VPS 本机上的流量,不对外机提供代理。
|
|
||||||
|
|
||||||
* **🐍 Python 脚本中使用代理**:
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
proxies = {
|
|
||||||
"http": "http://127.0.0.1:7928",
|
|
||||||
"https": "http://127.0.0.1:7928",
|
|
||||||
}
|
|
||||||
response = requests.get("https://www.google.com", proxies=proxies)
|
|
||||||
```
|
|
||||||
* **🐚 Shell 终端环境中使用代理**:
|
|
||||||
在命令行执行以下命令,可以让当前终端的后续命令(如 `curl`、`wget` 等)走代理出口:
|
|
||||||
```bash
|
|
||||||
export http_proxy="http://127.0.0.1:7928"
|
|
||||||
export https_proxy="http://127.0.0.1:7928"
|
|
||||||
```
|
|
||||||
* **⚙️ 本地其他服务配置**:
|
|
||||||
将本机的其他代理工具、爬虫框架或服务的出战代理设置为 `127.0.0.1:7928`。
|
|
||||||
|
|
||||||
> 💡 **小贴士**:如果您确实需要对公网其他设备开放此代理端口,可以通过设置环境变量 `export LOCAL_PROXY_HOST="::"` 重新启动服务以允许公网接入。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🛠️ 核心功能与操作说明
|
|
||||||
|
|
||||||
* **合并操作面板**:将“更新节点”与“立即检测补齐”合并,一键触发多线程拉取与测速。
|
|
||||||
* **正式版更新检测**:Web 顶部版本菜单可以检查 GitHub 最新稳定 Release;源码部署提示 `ml update`,Docker 部署提示重新拉取并启动镜像。
|
|
||||||
* **多国家发现范围**:节点表可实时勾选多个国家;点击“更新节点”后保存范围并影响后台周期拉取。
|
|
||||||
* **延迟来源区分**:实测延迟正常显示,官方 Ping 回退值使用弱化样式并标注为预估。
|
|
||||||
* **网关状态面板**:
|
|
||||||
- **系统诊断**:检测网关心跳及后台各个子守护线程(网页服务、VPN连接管理、出站网关服务)是否正常运行。若有脚本未运行,会提示具体的异常原因。
|
|
||||||
- **本地代理出口检测**:在网页端直接一键检测 VPS 后台对海外的实际连通状况,并回显真实的代理出站 IP 和所在地理位置。
|
|
||||||
* **日志追踪面板**:
|
|
||||||
- **分类过滤**:可精准筛选查看特定功能的日志(如 VPN 连接日志、API 请求日志、系统异常等)。
|
|
||||||
- **实时滚动与管理**:日志实时滚动加载,支持一键复制代码、一键导出 `.log` 日志文件到本地。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ⚠️ 小白安装与运行常见问题 (FAQ)
|
|
||||||
|
|
||||||
#### 1. 提示 `Cannot allocate tun` 或 `Cannot open tun/tap dev`
|
|
||||||
* **原因**:VPS 宿主机未启用虚拟网卡(TUN/TAP 设备)。这种情况常见于 LXC 或 OpenVZ 架构的轻量 VPS。
|
|
||||||
* **解决办法**:请登录您的 VPS 服务商控制面板(如 SolusVM/Proxmox),找到 **Enable TUN/TAP** / **开启 TUN** 选项并启用,然后重启 VPS。如无此选项,请工单联系客服开启。
|
|
||||||
|
|
||||||
#### 2. 网页管理后台无法打开(链接超时或拒绝连接)
|
|
||||||
* **原因 1**:VPS 本身自带防火墙(如 UFW、firewalld 或 iptables)阻断了管理端口(默认 `8787`)或代理端口(默认 `7928`)。
|
|
||||||
* **解决办法 1**:请在终端放行对应端口:
|
|
||||||
* **UFW (Ubuntu/Debian)**: `ufw allow 8787/tcp && ufw allow 7928/tcp`
|
|
||||||
* **Firewalld (CentOS/RHEL)**: `firewall-cmd --zone=public --add-port=8787/tcp --permanent && firewall-cmd --zone=public --add-port=7928/tcp --permanent && firewall-cmd --reload`
|
|
||||||
* **原因 2**:云服务商的“安全组”或“网络访问控制列表 (ACL)”未放行端口。
|
|
||||||
* **解决办法 2**:**非常重要!** 登录云服务商控制台(如阿里云、腾讯云、AWS、Oracle Cloud等),找到您 VPS 实例的 **安全组规则 (Security Group)**,在入站规则中添加:
|
|
||||||
- **协议类型**: `TCP`
|
|
||||||
- **端口范围**: `8787` (管理网页) 和 `7928` (代理端口)
|
|
||||||
- **授权对象/源IP**: `0.0.0.0/0` (允许所有人,或指定您自己的家庭公网 IP 提高安全性)
|
|
||||||
|
|
||||||
#### 3. 页面提示 `API Domain Blocked` 且备选节点显示为 0
|
|
||||||
* **原因**:您的 VPS DNS 解析异常,或者官方 VPNGate 域名遭防火墙拦截污染,导致无法下载节点列表。
|
|
||||||
* **解决办法**:
|
|
||||||
* **设置上游代理**:如果您有其他可用的代理服务,可在网页管理面板中打开“管理员 -> 代理及网络设置”,配置有效的 HTTP/SOCKS5 上游代理,后台会自动通过该代理拉取更新。
|
|
||||||
* **修改 DNS 解析器**:在终端修改 `/etc/resolv.conf`,将域名服务器替换为公共 DNS(如 `nameserver 8.8.8.8` 和 `nameserver 1.1.1.1`)。
|
|
||||||
|
|
||||||
程序会按以下顺序自动回退,不需要用户手动切换:
|
|
||||||
|
|
||||||
1. VPNGate 官方 HTTPS
|
|
||||||
2. VPNGate 官方 HTTP(兼容旧系统,结果不会覆盖 HTTPS 获得的可信缓存)
|
|
||||||
3. GitHub Pages 镜像 HTTPS
|
|
||||||
4. GitHub Pages 镜像 HTTP
|
|
||||||
5. VPS 本地最近有效快照;首次安装时使用仓库附带的初始快照
|
|
||||||
|
|
||||||
默认镜像为 `https://baoweise-bot.github.io/aimili-vpngate/vpngate.csv`。仓库管理员需要在 GitHub 的 **Settings -> Pages** 中将 Source 设置为 **GitHub Actions**,定时工作流会每 15 分钟校验并发布一次快照。可通过 `VPNGATE_API_HTTPS_URL`、`VPNGATE_API_HTTP_URL`、`VPNGATE_MIRROR_HTTPS_URL` 和 `VPNGATE_MIRROR_HTTP_URL` 覆盖各节点源。
|
|
||||||
|
|
||||||
#### 4. VPN 已成功连接,但客户端设置代理后无法上网 (无流量)
|
|
||||||
* **原因**:部分系统启用了严格的反向路径过滤(`rp_filter`),导致策略路由的入站/出站数据包被系统误判丢弃。
|
|
||||||
* **解决办法**:在终端输入 `ml` 命令打开交互菜单,工具会自动检测并提示您将 `rp_filter` 修复为宽松模式(值为 `2`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🎁 捐赠支持项目开发
|
|
||||||
|
|
||||||
如果您觉得这个项目对您有所帮助,欢迎捐赠支持我们的后续开发与维护:
|
|
||||||
|
|
||||||
* **BNB (BSC / BEP20)**: `0xB6d78c42CEB0687A31B8cfEBE4b51b6eB8953C17`
|
|
||||||
* **TRX (TRC20)**: `TSdzCW6JvsrqcppodYjhSrku4mYmDJ9pxf`
|
|
||||||
|
|
||||||
感谢您的慷慨与支持!❤️
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<a name="english"></a>
|
|
||||||
## English
|
|
||||||
|
|
||||||
AimiliVPN is a high-performance, zero-dependency VPN proxy gateway built entirely using Python's standard library. It parses official VPNGate servers, benchmarks latency, and routes traffic through a built-in dual-protocol (HTTP/SOCKS5) proxy server.
|
|
||||||
|
|
||||||
### 🌟 Recommended VPS Deals
|
|
||||||
[](https://bandwagonhost.com/aff.php?aff=81790)
|
|
||||||
[](https://my.racknerd.com/aff.php?aff=18708)
|
|
||||||
|
|
||||||
| Pick | Best for | Highlights | Link |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| **BandwagonHost** | Users who care most about China connectivity, latency, and route quality | **Premium China Telecom/Unicom/Mobile optimized routes**, ideal for demanding cross-border networking and long-term use | [View deals](https://bandwagonhost.com/aff.php?aff=81790) |
|
|
||||||
| **RackNerd** | Budget deployments, testing, and long-running lightweight services | **6000GB monthly bandwidth**, affordable pricing, and generous specs for value-focused VPS use | [View deals](https://my.racknerd.com/aff.php?aff=18708) |
|
|
||||||
|
|
||||||
|
|
||||||
### 📢 Community & Feedback
|
|
||||||
- **Telegram Group**: [arestemple](https://t.me/arestemple)
|
|
||||||
- **Discussion Forum**: [339936.xyz](https://339936.xyz)
|
|
||||||
- **Video Tutorial**: [YouTube Guide](https://www.youtube.com/watch?v=s-ATfXR8BpI)
|
|
||||||
- **Email Contact**: yaohunse7@gmail.com
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🚀 One-Click Installation
|
|
||||||
|
|
||||||
Run the corresponding command on your Linux VPS as root:
|
|
||||||
|
|
||||||
#### 🌟 V2.1 Formal Release (main branch only)
|
|
||||||
```bash
|
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
> 💡 **Quick Note**: Once installed, copy the printed URL from the terminal to access the Web UI. Type the `ml` command in the terminal to summon the interactive CLI management console.
|
|
||||||
|
|
||||||
#### Docker / Docker Compose
|
|
||||||
|
|
||||||
GitHub publishes prebuilt images for `linux/amd64`, `linux/386`, `linux/arm64`, and `linux/arm/v7` under `ghcr.io/baoweise-bot/aimili-vpngate:2.1`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
docker compose build
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
To build natively on the VPS instead, run `docker compose build` before `docker compose up -d`. Docker requires a Linux host with `/dev/net/tun`, `NET_ADMIN`, and `NET_RAW` support.
|
</details>
|
||||||
|
|
||||||
---
|
<a id="connection"></a>
|
||||||
|
## 连接与使用
|
||||||
|
|
||||||
### 💡 Quick Start Guide
|
### 1. 登录 Web 后台
|
||||||
|
|
||||||
#### Step 1: Access the Web UI
|
源码安装完成后,使用终端输出的地址访问:
|
||||||
Open your browser and navigate to the printed URL (e.g. `http://your_vps_ip:8787/u71e9IXp4TPx`).
|
|
||||||
|
|
||||||
#### Step 2: Select Node and Mode
|
```text
|
||||||
1. Wait for the program to complete its first automatic node speed benchmarks.
|
http://VPS_IP:8787/随机安全路径/
|
||||||
2. Under "Admin", you can trigger node fetching. The backend concurrently tests official VPNGate nodes and ranks them by latency.
|
```
|
||||||
3. Switch routes mode (Smart Auto, Specific Region, or Specific Server Node) according to your needs.
|
|
||||||
|
|
||||||
#### Step 3: Use Localhost Proxy (Core Step)
|
忘记地址时执行 `ml status`;需要重设账号密码时执行 `ml password`。
|
||||||
To prevent unauthorized scanning and abuse of the proxy port on the public internet, the built-in HTTP/SOCKS5 proxy server (default port **`7928`**) **binds to localhost (`127.0.0.1`) by default**. It is designed to route traffic generated locally on the VPS, rather than acting as a public proxy server.
|
|
||||||
|
|
||||||
* **🐍 Proxy in Python**:
|
Docker 用户可以读取首次启动时保存的 Web 配置:
|
||||||
```python
|
|
||||||
import requests
|
```bash
|
||||||
proxies = {
|
docker exec aimilivpn cat /data/ui_auth.json
|
||||||
|
```
|
||||||
|
|
||||||
|
使用其中的 `secret_path`、`username` 和 `password` 登录,并在首次登录后修改安全路径和凭据。
|
||||||
|
|
||||||
|
### 2. 获取并连接节点
|
||||||
|
|
||||||
|
1. 登录后台,等待首次节点加载完成,或点击“更新节点”。
|
||||||
|
2. 按国家筛选节点,并使用“测试”检查本机实测延迟与可用性。
|
||||||
|
3. 点击目标节点的“切换”;目标预检失败时,程序会尽量保留当前可用连接。
|
||||||
|
4. 根据需要选择智能自动、固定国家或固定 IP 模式。
|
||||||
|
5. 在状态区域确认 VPN 已连接,并核对当前出口 IP。
|
||||||
|
|
||||||
|
### 3. 在 VPS 本机使用代理
|
||||||
|
|
||||||
|
HTTP、HTTPS 网站代理和 SOCKS5 共用 `127.0.0.1:7928`。HTTPS 网站通过 HTTP 代理的 `CONNECT` 方法访问,代理地址仍填写 `http://127.0.0.1:7928`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTP / HTTPS
|
||||||
|
curl -x http://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
|
||||||
|
# SOCKS5,并通过代理解析域名
|
||||||
|
curl --proxy socks5h://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>查看 Shell 环境变量与 Python 示例</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export http_proxy="http://127.0.0.1:7928"
|
||||||
|
export https_proxy="http://127.0.0.1:7928"
|
||||||
|
curl https://api.ipify.org
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
proxies = {
|
||||||
"http": "http://127.0.0.1:7928",
|
"http": "http://127.0.0.1:7928",
|
||||||
"https": "http://127.0.0.1:7928",
|
"https": "http://127.0.0.1:7928",
|
||||||
}
|
}
|
||||||
response = requests.get("https://www.google.com", proxies=proxies)
|
|
||||||
```
|
|
||||||
* **🐚 Proxy in Shell terminal**:
|
|
||||||
```bash
|
|
||||||
export http_proxy="http://127.0.0.1:7928"
|
|
||||||
export https_proxy="http://127.0.0.1:7928"
|
|
||||||
```
|
|
||||||
* **⚙️ Other local services**:
|
|
||||||
Configure your scrapers, frameworks, or utility tools on this VPS to send traffic via `127.0.0.1:7928`.
|
|
||||||
|
|
||||||
> 💡 **Quick Note**: If you really need to open this proxy port to the public internet, you can set the environment variable `export LOCAL_PROXY_HOST="::"` before running the manager.
|
response = requests.get("https://api.ipify.org", proxies=proxies, timeout=20)
|
||||||
|
print(response.text)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
</details>
|
||||||
|
|
||||||
### ⚠️ Common Troubleshooting (FAQ)
|
### 4. 从电脑或其他设备连接
|
||||||
|
|
||||||
#### 1. Error: `Cannot allocate tun` or `Cannot open tun/tap dev`
|
代理默认只监听 VPS 回环地址。推荐使用 SSH 隧道,不要直接暴露代理端口:
|
||||||
* **Reason**: Virtual network adapter (TUN/TAP device) is disabled. This is common in OpenVZ/LXC VPS instances.
|
|
||||||
* **Solution**: Enable **TUN/TAP** in your VPS SolusVM/KiwiVM control panel, or submit a support ticket to your hosting provider.
|
|
||||||
|
|
||||||
#### 2. Cannot open the Web UI in the browser
|
```bash
|
||||||
* **Reason 1**: The built-in firewall (UFW or firewalld) is blocking ports `8787` (Web UI) and `7928` (Proxy).
|
ssh -N \
|
||||||
* **Solution 1**: Allow the ports in your OS firewall:
|
-L 8787:127.0.0.1:8787 \
|
||||||
* **UFW**: `ufw allow 8787/tcp && ufw allow 7928/tcp`
|
-L 7928:127.0.0.1:7928 \
|
||||||
* **Firewalld**: `firewall-cmd --add-port=8787/tcp --permanent && firewall-cmd --add-port=7928/tcp --permanent && firewall-cmd --reload`
|
root@VPS_IP
|
||||||
* **Reason 2**: Service provider security group blocking ports.
|
```
|
||||||
* **Solution 2**: **Crucial!** Log in to your cloud provider console (AWS, Aliyun, Oracle Cloud, etc.), locate the **Security Group** for your instance, and add an inbound TCP rule to allow ports `8787` and `7928` from `0.0.0.0/0`.
|
|
||||||
|
|
||||||
#### 3. "API Domain Blocked" / Candidate nodes pool is empty (0 nodes)
|
隧道建立后:
|
||||||
* **Reason**: The official VPNGate domain is blocked or DNS resolution failed on your VPS.
|
|
||||||
* **Solution**: Add an HTTP/SOCKS5 upstream proxy in the settings panel (Admin -> Proxy Settings), or configure public DNS in `/etc/resolv.conf` (e.g., `nameserver 8.8.8.8`).
|
|
||||||
|
|
||||||
The application automatically tries the official HTTPS endpoint, official HTTP endpoint, GitHub Pages HTTPS mirror, GitHub Pages HTTP mirror, and finally the last valid local snapshot. A validated initial snapshot is bundled for first startup. HTTP results remain supported for older systems but do not replace the cache obtained through HTTPS.
|
- Web:`http://127.0.0.1:8787/随机安全路径/`
|
||||||
|
- HTTP / HTTPS 代理:`127.0.0.1:7928`
|
||||||
|
- SOCKS5 代理:`127.0.0.1:7928`,支持时选择远程 DNS 或 `socks5h`
|
||||||
|
|
||||||
The default mirror is `https://baoweise-bot.github.io/aimili-vpngate/vpngate.csv`. Repository administrators must select **GitHub Actions** as the Pages source under **Settings -> Pages**. The scheduled workflow validates and publishes a fresh snapshot every 15 minutes. Source URLs can be overridden with `VPNGATE_API_HTTPS_URL`, `VPNGATE_API_HTTP_URL`, `VPNGATE_MIRROR_HTTPS_URL`, and `VPNGATE_MIRROR_HTTP_URL`.
|
> [!WARNING]
|
||||||
|
> `7928` 默认没有面向公网的用户认证。请勿在没有防火墙、来源 IP 限制或其他可靠访问控制的情况下将其直接开放到公网。
|
||||||
|
|
||||||
---
|
<a id="community"></a>
|
||||||
|
## 网站、社群与视频
|
||||||
|
|
||||||
### 🎁 Donation Support
|
| 入口 | 用途 | 链接 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 项目网站 / 交流论坛 | 公告、经验交流与讨论 | [339936.xyz](https://339936.xyz) |
|
||||||
|
| Telegram 群 | 即时交流 | [t.me/arestemple](https://t.me/arestemple) |
|
||||||
|
| YouTube 教程 | 安装和使用视频 | [观看视频](https://www.youtube.com/watch?v=s-ATfXR8BpI) |
|
||||||
|
| GitHub Issues | 可复现的问题与功能建议 | [提交 Issue](https://github.com/baoweise-bot/aimili-vpngate/issues) |
|
||||||
|
| 电子邮箱 | Bug 反馈与联系 | [yaohunse7@gmail.com](mailto:yaohunse7@gmail.com) |
|
||||||
|
|
||||||
If you find this project helpful, you can support its development and maintenance via donation:
|
<a id="legal"></a>
|
||||||
|
## 使用范围与法律声明
|
||||||
|
|
||||||
* **BNB (BSC / BEP20)**: `0xB6d78c42CEB0687A31B8cfEBE4b51b6eB8953C17`
|
> [!CAUTION]
|
||||||
* **TRX (TRC20)**: `TSdzCW6JvsrqcppodYjhSrku4mYmDJ9pxf`
|
> 下载、部署或使用本项目即表示您应自行确认用途符合所在地法律、VPS 所在地法律、网络服务商条款及 VPNGate 的相关规则。以下内容是项目使用边界,不构成法律意见,也不能保证免除任何个人或组织依法应承担的责任。
|
||||||
|
|
||||||
Thank you for your generosity and support! ❤️
|
1. **限定用途**:本项目仅用于合法的网络研究、教育、开发测试、隐私保护和经授权的网络访问,不得用于绕过依法实施的监管措施、未授权访问、攻击、扫描、垃圾信息、欺诈、侵权或其他违法活动。
|
||||||
|
2. **网络与地区限制**:不同地区和数据中心可能限制 VPNGate、GitHub 镜像或远端 VPN 节点。本项目不承诺任何地区或机型始终可用;仅应在当地法律和服务商条款允许的环境中合理使用。
|
||||||
|
3. **第三方节点**:VPNGate 节点由第三方志愿者运营,本项目不拥有、不控制也不审核这些节点,无法保证其稳定性、速度、安全性、隐私政策或日志行为。请勿通过不可信节点传输账号密码、金融信息、商业机密等敏感数据。
|
||||||
|
4. **用户责任**:节点选择、流量内容、部署位置、端口开放和账号安全均由使用者负责。因违法使用、配置不当、第三方节点、服务中断、数据泄露或账号滥用产生的后果,由使用者依法承担。
|
||||||
|
5. **无保证提供**:软件按“现状”提供,在适用法律允许的最大范围内,维护者不对可用性、适销性、特定用途适用性或间接损失作出保证。无法依法排除的责任不受本声明影响。
|
||||||
|
6. **不确定时停止使用**:如无法确认当地法律或服务商是否允许,请停止部署和使用,并咨询当地有执业资格的法律专业人士。
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[正式版本](https://github.com/baoweise-bot/aimili-vpngate/releases/latest) · [问题反馈](https://github.com/baoweise-bot/aimili-vpngate/issues) · [GPL-3.0 License](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
+40
-14
@@ -1,19 +1,45 @@
|
|||||||
# AimiliVPN V2.1 正式版
|
# AimiliVPN V2.1.2 正式版
|
||||||
|
|
||||||
V2.1 是仅从 `main` 主分支发布的首个正式版本标志。
|
V2.1.2 重点修复 IP 类型误判和部分 VPS 获取节点列表等待过久的问题,并增强 GitHub Pages、VPS 最近缓存和程序内置快照的回退体验。
|
||||||
|
|
||||||
## 本次更新
|
## Bug 修复
|
||||||
|
|
||||||
- 节点来源按“VPNGate 官方 HTTPS -> 官方 HTTP -> GitHub Pages HTTPS -> GitHub Pages HTTP -> VPS 本地快照 -> 内置初始快照”自动回退。
|
- 修复把 `proxy=true` 直接等同于机房 IP 的分类错误。住宅宽带用户运行 VPNGate 后可能被风险库标记为代理,但其网络归属仍然是住宅;现在代理属性与住宅/移动/机房类型分开保存。
|
||||||
- 修复节点获取缓慢、连接断开和切换失败时误伤现有连接的问题。
|
- 修复 Sony、Korea Telecom、JCOM、SK Broadband、KDDI、Cable TV 等消费宽带节点容易被误标为机房 IP 的问题。
|
||||||
- 恢复节点延迟列,区分本机实测值与 VPNGate 官方预估值。
|
- 保留对真实机房网络的识别:`hosting=true` 仍直接判为机房;SoftEther、hosting、cloud、server、data center、VPS 等明确数据中心供应商特征也仍判为机房。
|
||||||
- 加入国旗、实时多选国家筛选、国家范围持久化和单节点测试。
|
- 修复升级后旧版错误 IP 分类缓存继续生效最多 7 天的问题。分类缓存加入版本号,V2.1.2 会自动重新检测旧缓存,不需要用户手动删除运行数据。
|
||||||
- Web 管理端加入正式版更新检测,只检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示对应更新命令。
|
- 修复只给少量连通性检测成功节点补充 IP 类型、节点表中大部分节点长期显示未知的问题。后台任务现在会批量补全整个节点列表,同时只合并运营商和分类字段,不覆盖连接、延迟或检测状态。
|
||||||
- `install.sh` 和 `ml update` 统一只更新 `origin/main`。
|
- 修复 VPNGate 官方接口持续缓慢传输时可能突破原有 socket 超时、拖慢备用源切换的问题。每个网络节点源现在增加 6 秒总时限。
|
||||||
- GitHub Release 提供一个适用于 Linux `amd64`、`386`、`arm64`、`armv7` 的通用 Python 源码包与 SHA-256 校验文件。
|
- 修复官方 HTTPS 已超过总时限后仍继续等待同一主机 HTTP 的重复慢请求;超时后会直接尝试 GitHub Pages HTTPS。证书或 TLS 不兼容等非超时错误仍保留 HTTP 回退,兼容旧系统和不同 VPS 环境。
|
||||||
- GHCR 提供经过逐架构冒烟测试的 `amd64`、`386`、`arm64`、`arm/v7` Docker 镜像,并同时发布 `2.1.0`、`2.1`、`latest` 标签。
|
|
||||||
- Docker 用户默认拉取 GitHub 预构建镜像,也可以使用仓库中的 Dockerfile 在 VPS 本地构建。
|
|
||||||
|
|
||||||
## 兼容范围
|
## 节点源与镜像优化
|
||||||
|
|
||||||
应用依赖 Linux TUN、OpenVPN、iptables 和策略路由,因此正式支持 Linux 主机。Docker 也必须运行在具备 `/dev/net/tun` 的 Linux 主机上,并授予 `NET_ADMIN` 与 `NET_RAW` 能力。
|
- 节点顺序保持为:VPNGate 官方 HTTPS、官方 HTTP、GitHub Pages HTTPS、GitHub Pages HTTP、VPS 最近有效缓存、程序内置初始快照。
|
||||||
|
- GitHub Pages 定时同步从整刻 15 分钟调整为每小时第 7、22、37、52 分钟,降低 GitHub Actions 高峰期调度延迟概率。
|
||||||
|
- GitHub Pages 与官方 HTTPS 获取到的快照继续执行相同的 CSV 字段、大小、Base64 和 OpenVPN 危险指令校验。
|
||||||
|
- HTTP 节点源继续只作为兼容回退,不覆盖最后一次通过 HTTPS 获得的可信本地快照。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 40 项单元测试通过,覆盖住宅/代理分离、真实机房识别、旧缓存迁移、后台全量富化、连接状态保护和慢速官方源回退。
|
||||||
|
- Python 编译、前端 JavaScript 语法、`install.sh` 语法和 Docker Compose 配置检查通过。
|
||||||
|
- 测试 VPS 上 VPNGate 官方 HTTPS/HTTP、GitHub Pages HTTPS/HTTP、VPS 最近缓存和内置快照均能下载、解析并生成候选节点。
|
||||||
|
- 发布流水线对 Python 3.9、3.11、3.13 运行完整测试,并分别构建验证 `linux/amd64`、`linux/386`、`linux/arm64`、`linux/arm/v7`。
|
||||||
|
|
||||||
|
## 下载与更新
|
||||||
|
|
||||||
|
- GitHub Release 提供 `aimilivpn-v2.1.2-linux-source.tar.gz` 和 `sha256sums.txt`。
|
||||||
|
- GHCR 发布 `2.1.2`、`2.1`、`latest` 三组镜像标签。
|
||||||
|
|
||||||
|
Python 源码安装更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ml update
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker Compose 更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# AimiliVPN
|
||||||
|
|
||||||
|
**A VPNGate node manager and HTTP / HTTPS / SOCKS5 proxy gateway for Linux VPS hosts**
|
||||||
|
|
||||||
|
[](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)
|
||||||
|
[](https://github.com/baoweise-bot/aimili-vpngate/pkgs/container/aimili-vpngate)
|
||||||
|
[](../LICENSE)
|
||||||
|
|
||||||
|
[简体中文](../README.md) · **English** · [日本語](README.ja.md) · [한국어](README.ko.md)
|
||||||
|
|
||||||
|
[Quick install](#quick-install) · [Installation](#installation) · [Connection](#connection) · [VPS offers](#vps) · [Community](#community) · [Legal notice](#legal)
|
||||||
|
|
||||||
|
[](https://339936.xyz)
|
||||||
|
[](https://t.me/arestemple)
|
||||||
|
[](https://www.youtube.com/watch?v=s-ATfXR8BpI)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
AimiliVPN uses Python's standard library to manage VPNGate nodes. It provides node discovery and testing, connection switching, a Web dashboard, and HTTP, HTTPS website proxying, and SOCKS5 access on one local port.
|
||||||
|
|
||||||
|
| Item | Default or supported range |
|
||||||
|
| --- | --- |
|
||||||
|
| Web dashboard | TCP `8787`, a private path, username, and password |
|
||||||
|
| Local proxy | `127.0.0.1:7928`, HTTP, HTTPS `CONNECT`, and SOCKS5 |
|
||||||
|
| Source deployment | x64, x86, ARM64, and ARM32 Linux |
|
||||||
|
| Docker images | `linux/amd64`, `linux/386`, `linux/arm64`, and `linux/arm/v7` |
|
||||||
|
| Update channel | GitHub `main` stable branch / latest stable Release |
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Network availability:** Some regions, data centers, and network providers may restrict DNS, VPNGate APIs, GitHub mirrors, or VPN protocols. Mirrors and local snapshots improve node-list availability but cannot guarantee a successful connection on every host. Confirm that local law and your VPS provider permit VPN/TUN before deployment.
|
||||||
|
|
||||||
|
<a id="quick-install"></a>
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
Run as `root` on a supported Linux VPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer prints the complete Web URL, private path, username, and password. Run `ml` to open the management menu.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Enable TUN/TAP in the VPS control panel and verify that `/dev/net/tun` exists. The Web dashboard uses TCP `8787` by default; restrict firewall access to your own IP whenever possible.
|
||||||
|
|
||||||
|
<a id="vps"></a>
|
||||||
|
## VPS Offers
|
||||||
|
|
||||||
|
The following are affiliate links. Using them does not increase your price.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">BandwagonHost</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="CN2 GIA" src="https://img.shields.io/badge/China_Telecom-CN2_GIA-dc2626?style=flat-square">
|
||||||
|
<img alt="China Unicom 9929" src="https://img.shields.io/badge/China_Unicom-9929-f97316?style=flat-square">
|
||||||
|
<img alt="China Mobile CMIN2" src="https://img.shields.io/badge/China_Mobile-CMIN2-16a34a?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>Highlights</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Premium CN2 GIA, China Unicom 9929, and China Mobile CMIN2 optimized routes.</li>
|
||||||
|
<li>Low latency, high stability, and excellent cross-border route quality.</li>
|
||||||
|
<li>Strong connectivity for TikTok live operations, cross-border e-commerce, and long-running global services.</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://bandwagonhost.com/aff.php?aff=81790"><img alt="View BandwagonHost" src="https://img.shields.io/badge/View-BandwagonHost-dc2626?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">RackNerd</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="4000GB monthly traffic" src="https://img.shields.io/badge/Monthly_Traffic-4000GB-0284c7?style=flat-square">
|
||||||
|
<img alt="High traffic" src="https://img.shields.io/badge/Advantage-High_Traffic-0ea5e9?style=flat-square">
|
||||||
|
<img alt="Value" src="https://img.shields.io/badge/Pricing-High_Value-2563eb?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>Highlights</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>4000GB of monthly traffic with generous transfer headroom.</li>
|
||||||
|
<li>Affordable pricing and a strong traffic-to-cost ratio.</li>
|
||||||
|
<li>Low deployment cost for services that need to run continuously.</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://my.racknerd.com/aff.php?aff=18708"><img alt="View RackNerd" src="https://img.shields.io/badge/View-RackNerd-0284c7?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
Before purchasing, confirm that the selected plan permits TUN/TAP, OpenVPN, and the required network protocols. An affiliate link is not a guarantee that a specific plan will work.
|
||||||
|
|
||||||
|
<a id="installation"></a>
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Ubuntu, Debian, Alpine, CentOS, RHEL, Rocky Linux, AlmaLinux, Fedora, Oracle Linux, or Amazon Linux.
|
||||||
|
- `root`, OpenVPN, iptables, policy routing, and TUN/TAP.
|
||||||
|
- Windows and macOS can be proxy clients, but cannot run the full gateway. Docker Desktop is not equivalent to a Linux VPS with host TUN access.
|
||||||
|
|
||||||
|
### Option 1: Source installer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer deploys to `/opt/aimilivpn` and registers a system service.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ml # Open the management menu
|
||||||
|
ml status # Show status, Web URL, and username
|
||||||
|
ml logs # Follow logs
|
||||||
|
ml restart # Restart the service
|
||||||
|
ml password # Reset Web credentials
|
||||||
|
ml update # Update from the stable main branch
|
||||||
|
ml uninstall # Uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
To inspect the installer first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
sudo bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Universal Linux source archives and SHA-256 checksums are available from [GitHub Releases](https://github.com/baoweise-bot/aimili-vpngate/releases/latest). Version changes are documented in the Release Notes.
|
||||||
|
|
||||||
|
### Option 2: Docker Compose
|
||||||
|
|
||||||
|
The Docker host must provide `/dev/net/tun`, host networking, `NET_ADMIN`, and `NET_RAW`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
docker logs -f aimilivpn
|
||||||
|
```
|
||||||
|
|
||||||
|
Image: `ghcr.io/baoweise-bot/aimili-vpngate:2.1`
|
||||||
|
|
||||||
|
Update:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Show the docker run command</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name aimilivpn \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network host \
|
||||||
|
--cap-add NET_ADMIN \
|
||||||
|
--cap-add NET_RAW \
|
||||||
|
--device /dev/net/tun:/dev/net/tun \
|
||||||
|
-e UI_HOST=0.0.0.0 \
|
||||||
|
-e UI_PORT=8787 \
|
||||||
|
-e LOCAL_PROXY_HOST=127.0.0.1 \
|
||||||
|
-e LOCAL_PROXY_PORT=7928 \
|
||||||
|
-v aimilivpn-data:/data \
|
||||||
|
ghcr.io/baoweise-bot/aimili-vpngate:2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Build locally when GHCR is unavailable</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<a id="connection"></a>
|
||||||
|
## Connection and Use
|
||||||
|
|
||||||
|
### 1. Sign in to the Web dashboard
|
||||||
|
|
||||||
|
For a source installation, open the URL printed by the installer:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://VPS_IP:8787/private_path/
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `ml status` to recover the URL, or `ml password` to reset credentials.
|
||||||
|
|
||||||
|
Docker users can read the initial Web configuration with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec aimilivpn cat /data/ui_auth.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Use its `secret_path`, `username`, and `password`, then change them after the first sign-in.
|
||||||
|
|
||||||
|
### 2. Fetch and connect to a node
|
||||||
|
|
||||||
|
1. Sign in and wait for the first node list, or select **Update nodes**.
|
||||||
|
2. Filter by country and use **Test** to measure reachability and latency from the VPS.
|
||||||
|
3. Select **Switch** on the target node. A failed target precheck should leave the current usable connection in place when possible.
|
||||||
|
4. Choose Smart Auto, Fixed Country, or Fixed IP routing.
|
||||||
|
5. Confirm the VPN state and outbound IP in the status area.
|
||||||
|
|
||||||
|
### 3. Use the proxy on the VPS
|
||||||
|
|
||||||
|
HTTP, HTTPS website proxying, and SOCKS5 share `127.0.0.1:7928`. HTTPS sites use the HTTP proxy's `CONNECT` method, so the proxy URL remains `http://127.0.0.1:7928`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTP / HTTPS
|
||||||
|
curl -x http://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
|
||||||
|
# SOCKS5 with remote DNS resolution
|
||||||
|
curl --proxy socks5h://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Connect from another computer
|
||||||
|
|
||||||
|
The proxy listens on the VPS loopback address by default. Use an SSH tunnel instead of exposing it publicly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -N \
|
||||||
|
-L 8787:127.0.0.1:8787 \
|
||||||
|
-L 7928:127.0.0.1:7928 \
|
||||||
|
root@VPS_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
After the tunnel is established:
|
||||||
|
|
||||||
|
- Web: `http://127.0.0.1:8787/private_path/`
|
||||||
|
- HTTP / HTTPS proxy: `127.0.0.1:7928`
|
||||||
|
- SOCKS5 proxy: `127.0.0.1:7928`; enable remote DNS or `socks5h` when available
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Port `7928` has no public-facing user authentication by default. Never expose it directly without a firewall, source-IP restriction, or another reliable access-control layer.
|
||||||
|
|
||||||
|
<a id="community"></a>
|
||||||
|
## Website, Community, and Video
|
||||||
|
|
||||||
|
| Destination | Purpose | Link |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Website / forum | Announcements and discussion | [339936.xyz](https://339936.xyz) |
|
||||||
|
| Telegram group | Real-time community chat | [t.me/arestemple](https://t.me/arestemple) |
|
||||||
|
| YouTube tutorial | Installation and usage video | [Watch](https://www.youtube.com/watch?v=s-ATfXR8BpI) |
|
||||||
|
| GitHub Issues | Reproducible bugs and feature requests | [Open an issue](https://github.com/baoweise-bot/aimili-vpngate/issues) |
|
||||||
|
| Email | Bug reports and contact | [yaohunse7@gmail.com](mailto:yaohunse7@gmail.com) |
|
||||||
|
|
||||||
|
<a id="legal"></a>
|
||||||
|
## Scope of Use and Legal Notice
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> By downloading, deploying, or using this project, you are responsible for confirming compliance with the laws of your location, the laws where the VPS is hosted, provider terms, and applicable VPNGate rules. This section defines project boundaries, is not legal advice, and cannot guarantee exemption from any liability imposed by law.
|
||||||
|
|
||||||
|
1. **Permitted purpose:** Use only for lawful network research, education, development testing, privacy protection, and authorized access. Do not use it to evade lawfully imposed controls, gain unauthorized access, attack or scan systems, send spam, commit fraud, infringe rights, or conduct any unlawful activity.
|
||||||
|
2. **Network and regional restrictions:** Some regions and data centers may restrict VPNGate, GitHub mirrors, or remote VPN nodes. The project does not guarantee continuous availability in any region or on any host. Use it only where local law and provider terms permit.
|
||||||
|
3. **Third-party nodes:** VPNGate nodes are operated by third-party volunteers. This project does not own, control, or audit them and cannot guarantee availability, speed, security, privacy practices, or logging behavior. Do not send passwords, financial information, trade secrets, or other sensitive data through untrusted nodes.
|
||||||
|
4. **User responsibility:** The user is responsible for node selection, traffic, deployment location, exposed ports, and account security, and bears legal responsibility for unlawful use, misconfiguration, third-party nodes, outages, data leaks, or account abuse.
|
||||||
|
5. **No warranty:** The software is provided “as is.” To the maximum extent permitted by applicable law, maintainers disclaim warranties of availability, merchantability, fitness for a particular purpose, and indirect damages. Liability that cannot lawfully be excluded remains unaffected.
|
||||||
|
6. **Stop if uncertain:** If you cannot confirm that local law and provider policy permit this use, do not deploy or use the software and consult a qualified lawyer in the relevant jurisdiction.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[Stable Release](https://github.com/baoweise-bot/aimili-vpngate/releases/latest) · [Issues](https://github.com/baoweise-bot/aimili-vpngate/issues) · [GPL-3.0 License](../LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# AimiliVPN
|
||||||
|
|
||||||
|
**Linux VPS 向け VPNGate ノード管理・HTTP / HTTPS / SOCKS5 プロキシゲートウェイ**
|
||||||
|
|
||||||
|
[](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)
|
||||||
|
[](https://github.com/baoweise-bot/aimili-vpngate/pkgs/container/aimili-vpngate)
|
||||||
|
[](../LICENSE)
|
||||||
|
|
||||||
|
[简体中文](../README.md) · [English](README.en.md) · **日本語** · [한국어](README.ko.md)
|
||||||
|
|
||||||
|
[クイックインストール](#quick-install) · [インストール](#installation) · [接続方法](#connection) · [VPS](#vps) · [コミュニティ](#community) · [法的通知](#legal)
|
||||||
|
|
||||||
|
[](https://339936.xyz)
|
||||||
|
[](https://t.me/arestemple)
|
||||||
|
[](https://www.youtube.com/watch?v=s-ATfXR8BpI)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
AimiliVPN は Python 標準ライブラリで VPNGate ノードを管理し、ノード取得・テスト、接続切り替え、Web 管理画面、HTTP / HTTPS サイト用プロキシと SOCKS5 を提供します。
|
||||||
|
|
||||||
|
| 項目 | デフォルトまたは対応範囲 |
|
||||||
|
| --- | --- |
|
||||||
|
| Web 管理画面 | TCP `8787`、専用パス、ユーザー名、パスワード |
|
||||||
|
| ローカルプロキシ | `127.0.0.1:7928`、HTTP、HTTPS `CONNECT`、SOCKS5 |
|
||||||
|
| ソース版 | x64、x86、ARM64、ARM32 Linux |
|
||||||
|
| Docker | `linux/amd64`、`linux/386`、`linux/arm64`、`linux/arm/v7` |
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **ネットワーク可用性:** 地域、データセンター、ネットワーク事業者によっては、DNS、VPNGate API、GitHub ミラー、VPN プロトコルが制限される場合があります。ミラーとローカルスナップショットはノード一覧の可用性を高めますが、すべての環境で接続を保証するものではありません。展開前に、現地法と VPS 事業者が VPN/TUN を許可していることを確認してください。
|
||||||
|
|
||||||
|
<a id="quick-install"></a>
|
||||||
|
## クイックインストール
|
||||||
|
|
||||||
|
対応する Linux VPS で `root` として実行します。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
完了後、Web 管理画面の URL、専用パス、ユーザー名、パスワードが表示されます。`ml` で管理メニューを開けます。
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> VPS の管理画面で TUN/TAP を有効にし、`/dev/net/tun` が存在することを確認してください。Web のデフォルトポートは TCP `8787` です。可能な限り自分の IP だけを許可してください。
|
||||||
|
|
||||||
|
<a id="vps"></a>
|
||||||
|
## VPS の案内
|
||||||
|
|
||||||
|
以下はアフィリエイトリンクです。リンク経由でも購入価格は上がりません。
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">BandwagonHost</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="CN2 GIA" src="https://img.shields.io/badge/China_Telecom-CN2_GIA-dc2626?style=flat-square">
|
||||||
|
<img alt="China Unicom 9929" src="https://img.shields.io/badge/China_Unicom-9929-f97316?style=flat-square">
|
||||||
|
<img alt="China Mobile CMIN2" src="https://img.shields.io/badge/China_Mobile-CMIN2-16a34a?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>主な特長</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>CN2 GIA、China Unicom 9929、China Mobile CMIN2 の高品質な最適化回線。</li>
|
||||||
|
<li>低遅延、高い安定性、優れた国際経路品質。</li>
|
||||||
|
<li>TikTok ライブ運営、越境 EC、長期的な海外向け事業に適した回線。</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://bandwagonhost.com/aff.php?aff=81790"><img alt="BandwagonHost" src="https://img.shields.io/badge/View-BandwagonHost-dc2626?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">RackNerd</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="4000GB monthly traffic" src="https://img.shields.io/badge/Monthly_Traffic-4000GB-0284c7?style=flat-square">
|
||||||
|
<img alt="High traffic" src="https://img.shields.io/badge/Advantage-High_Traffic-0ea5e9?style=flat-square">
|
||||||
|
<img alt="Value" src="https://img.shields.io/badge/Pricing-High_Value-2563eb?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>主な特長</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>月間 4000GB の大容量トラフィック。</li>
|
||||||
|
<li>手頃な価格と優れたトラフィック対コスト比。</li>
|
||||||
|
<li>継続稼働するサービスの導入コストを抑えやすい構成。</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://my.racknerd.com/aff.php?aff=18708"><img alt="RackNerd" src="https://img.shields.io/badge/View-RackNerd-0284c7?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
購入前に、対象プランが TUN/TAP、OpenVPN、必要なプロトコルを許可していることを確認してください。
|
||||||
|
|
||||||
|
<a id="installation"></a>
|
||||||
|
## インストール
|
||||||
|
|
||||||
|
### 動作要件
|
||||||
|
|
||||||
|
- Ubuntu、Debian、Alpine、CentOS、RHEL、Rocky Linux、AlmaLinux、Fedora、Oracle Linux、Amazon Linux。
|
||||||
|
- `root`、OpenVPN、iptables、ポリシールーティング、TUN/TAP。
|
||||||
|
- Windows と macOS はプロキシクライアントとして利用できますが、ゲートウェイ本体は実行できません。
|
||||||
|
|
||||||
|
### 方法 1:ソースインストーラー
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
`/opt/aimilivpn` に配置し、システムサービスを登録します。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ml # 管理メニュー
|
||||||
|
ml status # 状態、Web URL、ユーザー名
|
||||||
|
ml logs # ログ
|
||||||
|
ml restart # 再起動
|
||||||
|
ml password # Web 認証情報を再設定
|
||||||
|
ml update # main 安定版から更新
|
||||||
|
ml uninstall # アンインストール
|
||||||
|
```
|
||||||
|
|
||||||
|
事前にスクリプトを確認する場合:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
sudo bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux 共通ソースアーカイブと SHA-256 は [GitHub Releases](https://github.com/baoweise-bot/aimili-vpngate/releases/latest) から取得できます。変更内容は Release Notes に掲載されます。
|
||||||
|
|
||||||
|
### 方法 2:Docker Compose
|
||||||
|
|
||||||
|
Docker ホストには `/dev/net/tun`、host ネットワーク、`NET_ADMIN`、`NET_RAW` が必要です。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
docker logs -f aimilivpn
|
||||||
|
```
|
||||||
|
|
||||||
|
イメージ:`ghcr.io/baoweise-bot/aimili-vpngate:2.1`
|
||||||
|
|
||||||
|
更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>docker run コマンドを表示</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name aimilivpn \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network host \
|
||||||
|
--cap-add NET_ADMIN \
|
||||||
|
--cap-add NET_RAW \
|
||||||
|
--device /dev/net/tun:/dev/net/tun \
|
||||||
|
-e UI_HOST=0.0.0.0 \
|
||||||
|
-e UI_PORT=8787 \
|
||||||
|
-e LOCAL_PROXY_HOST=127.0.0.1 \
|
||||||
|
-e LOCAL_PROXY_PORT=7928 \
|
||||||
|
-v aimilivpn-data:/data \
|
||||||
|
ghcr.io/baoweise-bot/aimili-vpngate:2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>GHCR を利用できない場合のローカルビルド</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<a id="connection"></a>
|
||||||
|
## 接続と利用方法
|
||||||
|
|
||||||
|
### 1. Web 管理画面へログイン
|
||||||
|
|
||||||
|
ソース版では、インストーラーが表示した URL を開きます。
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://VPS_IP:8787/private_path/
|
||||||
|
```
|
||||||
|
|
||||||
|
URL は `ml status`、認証情報の再設定は `ml password` を使用します。Docker では次のコマンドで初期設定を確認できます。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec aimilivpn cat /data/ui_auth.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`secret_path`、`username`、`password` を使用し、初回ログイン後に変更してください。
|
||||||
|
|
||||||
|
### 2. ノードを取得して接続
|
||||||
|
|
||||||
|
1. ログイン後、最初の一覧を待つか「ノード更新」を実行します。
|
||||||
|
2. 国で絞り込み、「テスト」で VPS からの到達性と遅延を確認します。
|
||||||
|
3. 対象ノードの「切り替え」を選択します。事前確認に失敗した場合、可能な限り現在の接続を維持します。
|
||||||
|
4. スマート自動、国固定、IP 固定からルーティングモードを選びます。
|
||||||
|
5. 接続状態と出口 IP を確認します。
|
||||||
|
|
||||||
|
### 3. VPS 上でプロキシを使用
|
||||||
|
|
||||||
|
HTTP、HTTPS サイト用プロキシ、SOCKS5 は `127.0.0.1:7928` を共有します。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTP / HTTPS
|
||||||
|
curl -x http://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
|
||||||
|
# SOCKS5、DNS もプロキシ経由
|
||||||
|
curl --proxy socks5h://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 別の端末から接続
|
||||||
|
|
||||||
|
プロキシを公開せず、SSH トンネルを利用してください。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -N \
|
||||||
|
-L 8787:127.0.0.1:8787 \
|
||||||
|
-L 7928:127.0.0.1:7928 \
|
||||||
|
root@VPS_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
- Web:`http://127.0.0.1:8787/private_path/`
|
||||||
|
- HTTP / HTTPS:`127.0.0.1:7928`
|
||||||
|
- SOCKS5:`127.0.0.1:7928`。可能な場合はリモート DNS または `socks5h` を使用
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `7928` には、デフォルトで公開用のユーザー認証がありません。ファイアウォール、接続元 IP 制限などの確実なアクセス制御なしで公開しないでください。
|
||||||
|
|
||||||
|
<a id="community"></a>
|
||||||
|
## Web サイト・コミュニティ・動画
|
||||||
|
|
||||||
|
| 入口 | 用途 | リンク |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Web サイト / フォーラム | お知らせと交流 | [339936.xyz](https://339936.xyz) |
|
||||||
|
| Telegram | リアルタイム交流 | [t.me/arestemple](https://t.me/arestemple) |
|
||||||
|
| YouTube | インストール・利用方法 | [動画を見る](https://www.youtube.com/watch?v=s-ATfXR8BpI) |
|
||||||
|
| GitHub Issues | 再現可能な不具合と機能要望 | [Issue を作成](https://github.com/baoweise-bot/aimili-vpngate/issues) |
|
||||||
|
| メール | 不具合報告と連絡 | [yaohunse7@gmail.com](mailto:yaohunse7@gmail.com) |
|
||||||
|
|
||||||
|
<a id="legal"></a>
|
||||||
|
## 利用範囲と法的通知
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> 本プロジェクトをダウンロード、展開、使用する前に、利用地と VPS 所在地の法律、事業者の規約、VPNGate の規則への適合を利用者自身で確認してください。本節は利用範囲を示すもので、法律上の助言ではなく、法的責任の免除を保証するものではありません。
|
||||||
|
|
||||||
|
1. **許可される用途:** 合法なネットワーク研究、教育、開発テスト、プライバシー保護、許可されたアクセスに限ります。法的な規制の回避、不正アクセス、攻撃、スキャン、スパム、詐欺、権利侵害、その他の違法行為に使用してはいけません。
|
||||||
|
2. **ネットワークと地域の制限:** 地域やデータセンターによっては VPNGate、GitHub ミラー、VPN ノードが制限される場合があります。本プロジェクトは、いかなる地域・環境でも継続的な可用性を保証しません。現地法と事業者規約が許可する場合に限り使用してください。
|
||||||
|
3. **第三者ノード:** VPNGate ノードは第三者のボランティアが運営しています。本プロジェクトはノードを所有・管理・監査せず、可用性、速度、安全性、プライバシー方針、ログ動作を保証しません。機密情報を信頼できないノードで送信しないでください。
|
||||||
|
4. **利用者の責任:** ノード選択、通信内容、設置場所、公開ポート、アカウント管理は利用者の責任です。違法利用、設定不備、第三者ノード、停止、情報漏えい、不正利用に伴う責任は利用者が負います。
|
||||||
|
5. **無保証:** 本ソフトウェアは「現状有姿」で提供されます。適用法で認められる最大限の範囲で、保守者は可用性、商品性、特定目的適合性、間接損害を保証しません。法律上排除できない責任には影響しません。
|
||||||
|
6. **不明な場合:** 現地法や事業者規約で許可されているか確認できない場合は使用を中止し、該当法域の有資格法律専門家に相談してください。
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[Stable Release](https://github.com/baoweise-bot/aimili-vpngate/releases/latest) · [Issues](https://github.com/baoweise-bot/aimili-vpngate/issues) · [GPL-3.0 License](../LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# AimiliVPN
|
||||||
|
|
||||||
|
**Linux VPS용 VPNGate 노드 관리 및 HTTP / HTTPS / SOCKS5 프록시 게이트웨이**
|
||||||
|
|
||||||
|
[](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)
|
||||||
|
[](https://github.com/baoweise-bot/aimili-vpngate/pkgs/container/aimili-vpngate)
|
||||||
|
[](../LICENSE)
|
||||||
|
|
||||||
|
[简体中文](../README.md) · [English](README.en.md) · [日本語](README.ja.md) · **한국어**
|
||||||
|
|
||||||
|
[빠른 설치](#quick-install) · [설치](#installation) · [연결](#connection) · [VPS](#vps) · [커뮤니티](#community) · [법적 고지](#legal)
|
||||||
|
|
||||||
|
[](https://339936.xyz)
|
||||||
|
[](https://t.me/arestemple)
|
||||||
|
[](https://www.youtube.com/watch?v=s-ATfXR8BpI)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
AimiliVPN은 Python 표준 라이브러리로 VPNGate 노드를 관리하며 노드 검색과 테스트, 연결 전환, Web 관리 화면, HTTP / HTTPS 웹사이트 프록시 및 SOCKS5 접속을 제공합니다.
|
||||||
|
|
||||||
|
| 항목 | 기본값 또는 지원 범위 |
|
||||||
|
| --- | --- |
|
||||||
|
| Web 관리 화면 | TCP `8787`, 전용 경로, 사용자 이름, 비밀번호 |
|
||||||
|
| 로컬 프록시 | `127.0.0.1:7928`, HTTP, HTTPS `CONNECT`, SOCKS5 |
|
||||||
|
| 소스 배포 | x64, x86, ARM64, ARM32 Linux |
|
||||||
|
| Docker | `linux/amd64`, `linux/386`, `linux/arm64`, `linux/arm/v7` |
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **네트워크 가용성:** 일부 지역, 데이터 센터 및 네트워크 제공업체는 DNS, VPNGate API, GitHub 미러 또는 VPN 프로토콜을 제한할 수 있습니다. 미러와 로컬 스냅샷은 노드 목록 가용성을 높이지만 모든 환경의 연결 성공을 보장하지 않습니다. 배포 전에 현지 법률과 VPS 제공업체가 VPN/TUN을 허용하는지 확인하십시오.
|
||||||
|
|
||||||
|
<a id="quick-install"></a>
|
||||||
|
## 빠른 설치
|
||||||
|
|
||||||
|
지원되는 Linux VPS에서 `root`로 실행합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
설치 후 Web 관리 화면의 전체 URL, 전용 경로, 사용자 이름과 비밀번호가 표시됩니다. `ml` 명령으로 관리 메뉴를 열 수 있습니다.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> VPS 제어판에서 TUN/TAP을 활성화하고 `/dev/net/tun`이 존재하는지 확인하십시오. Web 기본 포트는 TCP `8787`이며 가능하면 자신의 IP만 허용하십시오.
|
||||||
|
|
||||||
|
<a id="vps"></a>
|
||||||
|
## VPS 안내
|
||||||
|
|
||||||
|
아래 링크는 제휴 링크이며, 이를 통해 구매해도 가격은 올라가지 않습니다.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">BandwagonHost</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="CN2 GIA" src="https://img.shields.io/badge/China_Telecom-CN2_GIA-dc2626?style=flat-square">
|
||||||
|
<img alt="China Unicom 9929" src="https://img.shields.io/badge/China_Unicom-9929-f97316?style=flat-square">
|
||||||
|
<img alt="China Mobile CMIN2" src="https://img.shields.io/badge/China_Mobile-CMIN2-16a34a?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>주요 특징</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>CN2 GIA, China Unicom 9929, China Mobile CMIN2 프리미엄 최적화 회선.</li>
|
||||||
|
<li>낮은 지연 시간, 높은 안정성, 뛰어난 국제 경로 품질.</li>
|
||||||
|
<li>TikTok 라이브 운영, 해외 전자상거래 및 장기 글로벌 서비스에 적합한 연결 품질.</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://bandwagonhost.com/aff.php?aff=81790"><img alt="BandwagonHost" src="https://img.shields.io/badge/View-BandwagonHost-dc2626?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<h3 align="center">RackNerd</h3>
|
||||||
|
<p align="center">
|
||||||
|
<img alt="4000GB monthly traffic" src="https://img.shields.io/badge/Monthly_Traffic-4000GB-0284c7?style=flat-square">
|
||||||
|
<img alt="High traffic" src="https://img.shields.io/badge/Advantage-High_Traffic-0ea5e9?style=flat-square">
|
||||||
|
<img alt="Value" src="https://img.shields.io/badge/Pricing-High_Value-2563eb?style=flat-square">
|
||||||
|
</p>
|
||||||
|
<p><strong>주요 특징</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>월 4000GB의 넉넉한 트래픽.</li>
|
||||||
|
<li>합리적인 가격과 뛰어난 트래픽 대비 비용 효율.</li>
|
||||||
|
<li>장기 실행 서비스의 배포 비용을 낮추기 좋은 구성.</li>
|
||||||
|
</ul>
|
||||||
|
<p align="center"><a href="https://my.racknerd.com/aff.php?aff=18708"><img alt="RackNerd" src="https://img.shields.io/badge/View-RackNerd-0284c7?style=for-the-badge"></a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
구매 전에 선택한 요금제가 TUN/TAP, OpenVPN 및 필요한 네트워크 프로토콜을 허용하는지 확인하십시오.
|
||||||
|
|
||||||
|
<a id="installation"></a>
|
||||||
|
## 설치
|
||||||
|
|
||||||
|
### 요구 사항
|
||||||
|
|
||||||
|
- Ubuntu, Debian, Alpine, CentOS, RHEL, Rocky Linux, AlmaLinux, Fedora, Oracle Linux 또는 Amazon Linux.
|
||||||
|
- `root`, OpenVPN, iptables, 정책 라우팅 및 TUN/TAP.
|
||||||
|
- Windows와 macOS는 프록시 클라이언트로 사용할 수 있지만 전체 게이트웨이를 실행할 수 없습니다.
|
||||||
|
|
||||||
|
### 방법 1: 소스 설치 프로그램
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
`/opt/aimilivpn`에 배포하고 시스템 서비스를 등록합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ml # 관리 메뉴
|
||||||
|
ml status # 상태, Web URL, 사용자 이름
|
||||||
|
ml logs # 로그 보기
|
||||||
|
ml restart # 서비스 재시작
|
||||||
|
ml password # Web 인증 정보 재설정
|
||||||
|
ml update # main 안정 브랜치에서 업데이트
|
||||||
|
ml uninstall # 제거
|
||||||
|
```
|
||||||
|
|
||||||
|
먼저 설치 프로그램을 검토하려면:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
sudo bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
공통 Linux 소스 아카이브와 SHA-256 체크섬은 [GitHub Releases](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)에서 받을 수 있습니다. 변경 사항은 Release Notes에 기록됩니다.
|
||||||
|
|
||||||
|
### 방법 2: Docker Compose
|
||||||
|
|
||||||
|
Docker 호스트는 `/dev/net/tun`, host 네트워크, `NET_ADMIN`, `NET_RAW`를 제공해야 합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
docker logs -f aimilivpn
|
||||||
|
```
|
||||||
|
|
||||||
|
이미지: `ghcr.io/baoweise-bot/aimili-vpngate:2.1`
|
||||||
|
|
||||||
|
업데이트:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>docker run 명령 보기</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name aimilivpn \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network host \
|
||||||
|
--cap-add NET_ADMIN \
|
||||||
|
--cap-add NET_RAW \
|
||||||
|
--device /dev/net/tun:/dev/net/tun \
|
||||||
|
-e UI_HOST=0.0.0.0 \
|
||||||
|
-e UI_PORT=8787 \
|
||||||
|
-e LOCAL_PROXY_HOST=127.0.0.1 \
|
||||||
|
-e LOCAL_PROXY_PORT=7928 \
|
||||||
|
-v aimilivpn-data:/data \
|
||||||
|
ghcr.io/baoweise-bot/aimili-vpngate:2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>GHCR을 사용할 수 없을 때 로컬 빌드</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch https://github.com/baoweise-bot/aimili-vpngate.git
|
||||||
|
cd aimili-vpngate
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<a id="connection"></a>
|
||||||
|
## 연결 및 사용
|
||||||
|
|
||||||
|
### 1. Web 관리 화면 로그인
|
||||||
|
|
||||||
|
소스 설치에서는 설치 프로그램이 출력한 URL을 엽니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://VPS_IP:8787/private_path/
|
||||||
|
```
|
||||||
|
|
||||||
|
URL은 `ml status`, 인증 정보 재설정은 `ml password`를 사용합니다. Docker 사용자는 다음 명령으로 초기 설정을 확인할 수 있습니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec aimilivpn cat /data/ui_auth.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`secret_path`, `username`, `password`로 로그인하고 첫 로그인 후 변경하십시오.
|
||||||
|
|
||||||
|
### 2. 노드 가져오기 및 연결
|
||||||
|
|
||||||
|
1. 로그인 후 첫 노드 목록을 기다리거나 “노드 업데이트”를 실행합니다.
|
||||||
|
2. 국가별로 필터링하고 “테스트”로 VPS에서의 연결 가능 여부와 지연 시간을 확인합니다.
|
||||||
|
3. 대상 노드의 “전환”을 선택합니다. 사전 확인에 실패하면 가능한 경우 현재 연결을 유지합니다.
|
||||||
|
4. 스마트 자동, 국가 고정 또는 IP 고정 라우팅을 선택합니다.
|
||||||
|
5. 상태 영역에서 VPN 연결과 출구 IP를 확인합니다.
|
||||||
|
|
||||||
|
### 3. VPS에서 프록시 사용
|
||||||
|
|
||||||
|
HTTP, HTTPS 웹사이트 프록시와 SOCKS5는 `127.0.0.1:7928`을 공유합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTP / HTTPS
|
||||||
|
curl -x http://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
|
||||||
|
# SOCKS5 및 원격 DNS
|
||||||
|
curl --proxy socks5h://127.0.0.1:7928 https://api.ipify.org
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 다른 컴퓨터에서 연결
|
||||||
|
|
||||||
|
프록시를 공개하지 말고 SSH 터널을 사용하십시오.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -N \
|
||||||
|
-L 8787:127.0.0.1:8787 \
|
||||||
|
-L 7928:127.0.0.1:7928 \
|
||||||
|
root@VPS_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
- Web: `http://127.0.0.1:8787/private_path/`
|
||||||
|
- HTTP / HTTPS: `127.0.0.1:7928`
|
||||||
|
- SOCKS5: `127.0.0.1:7928`, 가능하면 원격 DNS 또는 `socks5h` 사용
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `7928`은 기본적으로 공개 사용자 인증을 제공하지 않습니다. 방화벽, 접속 원본 IP 제한 또는 기타 신뢰할 수 있는 접근 제어 없이 공개하지 마십시오.
|
||||||
|
|
||||||
|
<a id="community"></a>
|
||||||
|
## 웹사이트, 커뮤니티 및 동영상
|
||||||
|
|
||||||
|
| 대상 | 용도 | 링크 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 웹사이트 / 포럼 | 공지와 토론 | [339936.xyz](https://339936.xyz) |
|
||||||
|
| Telegram 그룹 | 실시간 커뮤니티 | [t.me/arestemple](https://t.me/arestemple) |
|
||||||
|
| YouTube 튜토리얼 | 설치 및 사용 동영상 | [보기](https://www.youtube.com/watch?v=s-ATfXR8BpI) |
|
||||||
|
| GitHub Issues | 재현 가능한 버그와 기능 요청 | [Issue 만들기](https://github.com/baoweise-bot/aimili-vpngate/issues) |
|
||||||
|
| 이메일 | 버그 신고 및 연락 | [yaohunse7@gmail.com](mailto:yaohunse7@gmail.com) |
|
||||||
|
|
||||||
|
<a id="legal"></a>
|
||||||
|
## 사용 범위 및 법적 고지
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> 이 프로젝트를 다운로드, 배포 또는 사용하기 전에 사용 지역과 VPS 소재지의 법률, 제공업체 약관 및 VPNGate 규칙을 준수하는지 직접 확인해야 합니다. 이 내용은 프로젝트의 사용 범위를 설명할 뿐 법률 자문이 아니며 법적 책임의 면제를 보장하지 않습니다.
|
||||||
|
|
||||||
|
1. **허용 목적:** 합법적인 네트워크 연구, 교육, 개발 테스트, 개인정보 보호 및 승인된 접속에만 사용하십시오. 법에 따라 시행되는 통제 회피, 무단 접속, 공격, 스캔, 스팸, 사기, 권리 침해 또는 기타 불법 활동에 사용해서는 안 됩니다.
|
||||||
|
2. **네트워크 및 지역 제한:** 일부 지역과 데이터 센터는 VPNGate, GitHub 미러 또는 원격 VPN 노드를 제한할 수 있습니다. 이 프로젝트는 어떤 지역이나 호스트에서도 지속적인 가용성을 보장하지 않습니다. 현지 법률과 제공업체 약관이 허용하는 환경에서만 사용하십시오.
|
||||||
|
3. **제3자 노드:** VPNGate 노드는 제3자 자원봉사자가 운영합니다. 이 프로젝트는 해당 노드를 소유, 통제 또는 감사하지 않으며 가용성, 속도, 보안, 개인정보 처리 또는 로그 기록을 보장하지 않습니다. 신뢰할 수 없는 노드로 민감한 정보를 전송하지 마십시오.
|
||||||
|
4. **사용자 책임:** 노드 선택, 트래픽, 배포 위치, 공개 포트 및 계정 보안은 사용자의 책임입니다. 불법 사용, 잘못된 설정, 제3자 노드, 서비스 중단, 데이터 유출 또는 계정 남용에 대한 법적 책임은 사용자에게 있습니다.
|
||||||
|
5. **무보증:** 소프트웨어는 “있는 그대로” 제공됩니다. 적용 법률이 허용하는 최대 범위에서 유지관리자는 가용성, 상품성, 특정 목적 적합성 또는 간접 손해를 보증하지 않습니다. 법적으로 제외할 수 없는 책임에는 영향을 주지 않습니다.
|
||||||
|
6. **불확실한 경우 중단:** 현지 법률이나 제공업체 정책이 이를 허용하는지 확인할 수 없다면 배포 및 사용을 중단하고 해당 관할권의 자격 있는 법률 전문가와 상담하십시오.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[Stable Release](https://github.com/baoweise-bot/aimili-vpngate/releases/latest) · [Issues](https://github.com/baoweise-bot/aimili-vpngate/issues) · [GPL-3.0 License](../LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
+38
-13
@@ -186,18 +186,27 @@ INSTALL_DIR = "/opt/aimilivpn"
|
|||||||
LOG_FILE = "/opt/aimilivpn/vpngate_data/vpngate.log"
|
LOG_FILE = "/opt/aimilivpn/vpngate_data/vpngate.log"
|
||||||
|
|
||||||
def generate_random_password():
|
def generate_random_password():
|
||||||
import random
|
import secrets
|
||||||
import string
|
import string
|
||||||
chars = string.ascii_letters + string.digits
|
chars = string.ascii_letters + string.digits
|
||||||
while True:
|
while True:
|
||||||
pwd = "".join(random.choices(chars, k=12))
|
pwd = "".join(secrets.choice(chars) for _ in range(12))
|
||||||
if any(c.islower() for c in pwd) and any(c.isupper() for c in pwd) and any(c.isdigit() for c in pwd):
|
if any(c.islower() for c in pwd) and any(c.isupper() for c in pwd) and any(c.isdigit() for c in pwd):
|
||||||
return pwd
|
return pwd
|
||||||
|
|
||||||
def generate_random_suffix():
|
def generate_random_suffix():
|
||||||
import random
|
import secrets
|
||||||
import string
|
import string
|
||||||
return "".join(random.choices(string.ascii_letters + string.digits, k=12))
|
chars = string.ascii_letters + string.digits
|
||||||
|
return "".join(secrets.choice(chars) for _ in range(12))
|
||||||
|
|
||||||
|
def get_app_version():
|
||||||
|
try:
|
||||||
|
with open(os.path.join(INSTALL_DIR, "VERSION"), "r", encoding="utf-8") as f:
|
||||||
|
version = f.read().strip().lstrip("vV")
|
||||||
|
return version or "2.1.2"
|
||||||
|
except Exception:
|
||||||
|
return "2.1.2"
|
||||||
|
|
||||||
def load_ui_cfg():
|
def load_ui_cfg():
|
||||||
import json
|
import json
|
||||||
@@ -406,7 +415,7 @@ def print_status():
|
|||||||
openvpn_status = f"{green}[已连接]{reset}" if openvpn_ok else f"{red}[未连接]{reset}"
|
openvpn_status = f"{green}[已连接]{reset}" if openvpn_ok else f"{red}[未连接]{reset}"
|
||||||
|
|
||||||
print_line("=======================================================")
|
print_line("=======================================================")
|
||||||
print_line(f" {bold}AimiliVPN 管理终端 v2.0{reset} ")
|
print_line(f" {bold}AimiliVPN 管理终端 v{get_app_version()}{reset} ")
|
||||||
print_line("=======================================================")
|
print_line("=======================================================")
|
||||||
print_line("【核心服务状态】")
|
print_line("【核心服务状态】")
|
||||||
print_line(format_line(f"代理网关 (Port {proxy_port})", gateway_status))
|
print_line(format_line(f"代理网关 (Port {proxy_port})", gateway_status))
|
||||||
@@ -728,7 +737,7 @@ def configure_credentials():
|
|||||||
new_uname = input(f"请输入新管理账号 (回车默认 {curr_uname}): ").strip()
|
new_uname = input(f"请输入新管理账号 (回车默认 {curr_uname}): ").strip()
|
||||||
if not new_uname:
|
if not new_uname:
|
||||||
new_uname = curr_uname
|
new_uname = curr_uname
|
||||||
new_pwd = input("请输入新管理密码 (不能为空): ").strip()
|
new_pwd = input("请输入新管理密码 (不能为空): ")
|
||||||
if not new_pwd:
|
if not new_pwd:
|
||||||
print("错误: 密码不能为空!")
|
print("错误: 密码不能为空!")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@@ -965,22 +974,22 @@ if [ ! -f "$AUTH_FILE" ]; then
|
|||||||
# Initialize defaults
|
# Initialize defaults
|
||||||
UI_PORT=8787
|
UI_PORT=8787
|
||||||
# generate random secret suffix (12 chars alphanumeric)
|
# generate random secret suffix (12 chars alphanumeric)
|
||||||
SECRET_PATH=$(python3 -c "import random, string; print(''.join(random.choices(string.ascii_letters + string.digits, k=12)))")
|
SECRET_PATH=$(python3 -c "import secrets, string; chars = string.ascii_letters + string.digits; print(''.join(secrets.choice(chars) for _ in range(12)))")
|
||||||
# generate random password
|
# generate random password
|
||||||
UI_PASSWORD=$(python3 -c "
|
UI_PASSWORD=$(python3 -c "
|
||||||
import random, string
|
import secrets, string
|
||||||
chars = string.ascii_letters + string.digits
|
chars = string.ascii_letters + string.digits
|
||||||
while True:
|
while True:
|
||||||
pwd = ''.join(random.choices(chars, k=12))
|
pwd = ''.join(secrets.choice(chars) for _ in range(12))
|
||||||
if any(c.islower() for c in pwd) and any(c.isupper() for c in pwd) and any(c.isdigit() for c in pwd):
|
if any(c.islower() for c in pwd) and any(c.isupper() for c in pwd) and any(c.isdigit() for c in pwd):
|
||||||
print(pwd)
|
print(pwd)
|
||||||
break
|
break
|
||||||
")
|
")
|
||||||
UI_USERNAME=$(python3 -c "
|
UI_USERNAME=$(python3 -c "
|
||||||
import random, string
|
import secrets, string
|
||||||
chars = string.ascii_letters + string.digits
|
chars = string.ascii_letters + string.digits
|
||||||
while True:
|
while True:
|
||||||
uname = ''.join(random.choices(chars, k=12))
|
uname = ''.join(secrets.choice(chars) for _ in range(12))
|
||||||
if uname[0].isalpha() and any(c.islower() for c in uname) and any(c.isupper() for c in uname) and any(c.isdigit() for c in uname):
|
if uname[0].isalpha() and any(c.islower() for c in uname) and any(c.isupper() for c in uname) and any(c.isdigit() for c in uname):
|
||||||
print(uname)
|
print(uname)
|
||||||
break
|
break
|
||||||
@@ -1089,6 +1098,22 @@ if [ -d "/proc/sys/net/ipv4/conf" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "\n正在启动 AimiliVPN 服务并初始化网络..."
|
echo -e "\n正在启动 AimiliVPN 服务并初始化网络..."
|
||||||
|
# Avoid treating the previous process' persisted node ID as a successful new
|
||||||
|
# connection during upgrades. The service will replace this startup state.
|
||||||
|
if [ -f "${INSTALL_DIR}/vpngate_data/state.json" ]; then
|
||||||
|
python3 - "${INSTALL_DIR}/vpngate_data/state.json" <<'PY' 2>/dev/null || true
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
state_path = Path(sys.argv[1])
|
||||||
|
state = json.loads(state_path.read_text(encoding="utf-8"))
|
||||||
|
state["active_openvpn_node_id"] = ""
|
||||||
|
state["is_connecting"] = True
|
||||||
|
state["last_check_message"] = "服务正在重启并重新建立加密通道..."
|
||||||
|
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl restart aimilivpn.service || true
|
systemctl restart aimilivpn.service || true
|
||||||
elif command -v rc-service >/dev/null 2>&1; then
|
elif command -v rc-service >/dev/null 2>&1; then
|
||||||
@@ -1096,7 +1121,7 @@ elif command -v rc-service >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Wait and poll for node loading and active connection
|
# Wait and poll for node loading and active connection
|
||||||
echo -e "\n正在等待 AimiliVPN 首次获取节点并建立加密通道 (此过程可能需要 5-30 秒)..."
|
echo -e "\n正在等待 AimiliVPN 首次获取节点并建立加密通道 (此过程可能需要 5-90 秒)..."
|
||||||
ACTIVE_ID=""
|
ACTIVE_ID=""
|
||||||
LAST_MSG=""
|
LAST_MSG=""
|
||||||
for i in {1..90}; do
|
for i in {1..90}; do
|
||||||
@@ -1106,7 +1131,7 @@ for i in {1..90}; do
|
|||||||
CUR_MSG=$(python3 -c "import json; print(json.load(open('${INSTALL_DIR}/vpngate_data/state.json')).get('last_check_message', ''))" 2>/dev/null || echo "")
|
CUR_MSG=$(python3 -c "import json; print(json.load(open('${INSTALL_DIR}/vpngate_data/state.json')).get('last_check_message', ''))" 2>/dev/null || echo "")
|
||||||
|
|
||||||
if [ "$IS_CONN" = "False" ] || [ "$IS_CONN" = "false" ]; then
|
if [ "$IS_CONN" = "False" ] || [ "$IS_CONN" = "false" ]; then
|
||||||
if [ -n "$ACTIVE_ID" ]; then
|
if [ -n "$ACTIVE_ID" ] && ip link show dev tun0 >/dev/null 2>&1 && pidof openvpn >/dev/null 2>&1; then
|
||||||
echo -e " -> ${GREEN}[已就绪]${PLAIN} 首次节点连接成功,活动节点: ${GREEN}$ACTIVE_ID${PLAIN}"
|
echo -e " -> ${GREEN}[已就绪]${PLAIN} 首次节点连接成功,活动节点: ${GREEN}$ACTIVE_ID${PLAIN}"
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -254,6 +254,9 @@ def socks5_client(client: socket.socket, first_byte: bytes) -> None:
|
|||||||
return
|
return
|
||||||
client.sendall(b"\x01\x00")
|
client.sendall(b"\x01\x00")
|
||||||
else:
|
else:
|
||||||
|
if 0 not in methods:
|
||||||
|
client.sendall(b"\x05\xff")
|
||||||
|
return
|
||||||
client.sendall(b"\x05\x00")
|
client.sendall(b"\x05\x00")
|
||||||
version, command, _, address_type = recv_exact(client, 4)
|
version, command, _, address_type = recv_exact(client, 4)
|
||||||
if version != 5 or command != 1:
|
if version != 5 or command != 1:
|
||||||
|
|||||||
+254
-5
@@ -85,6 +85,8 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
mock.patch.object(manager, "API_CACHE_FILE", root / "api_snapshot.csv"),
|
mock.patch.object(manager, "API_CACHE_FILE", root / "api_snapshot.csv"),
|
||||||
mock.patch.object(manager, "API_CACHE_META_FILE", root / "api_snapshot.meta.json"),
|
mock.patch.object(manager, "API_CACHE_META_FILE", root / "api_snapshot.meta.json"),
|
||||||
mock.patch.object(manager, "BUNDLED_SNAPSHOT_FILE", root / "bundled_snapshot.csv"),
|
mock.patch.object(manager, "BUNDLED_SNAPSHOT_FILE", root / "bundled_snapshot.csv"),
|
||||||
|
mock.patch.object(manager.vpn_utils, "DATA_DIR", root),
|
||||||
|
mock.patch.object(manager.vpn_utils, "IP_CACHE_FILE", root / "ip_cache.json"),
|
||||||
]
|
]
|
||||||
for patcher in self.path_patches:
|
for patcher in self.path_patches:
|
||||||
patcher.start()
|
patcher.start()
|
||||||
@@ -98,6 +100,7 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
manager.last_proxy_failure_node_id = ""
|
manager.last_proxy_failure_node_id = ""
|
||||||
manager.background_refill_thread = None
|
manager.background_refill_thread = None
|
||||||
manager.background_refill_cancel_event.clear()
|
manager.background_refill_cancel_event.clear()
|
||||||
|
manager.active_sessions.clear()
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
if manager.connection_attempt_lock.locked():
|
if manager.connection_attempt_lock.locked():
|
||||||
@@ -155,6 +158,142 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
self.assertEqual(5, sum(node.get("probe_status") == "available" for node in stored))
|
self.assertEqual(5, sum(node.get("probe_status") == "available" for node in stored))
|
||||||
self.assertEqual(7, sum(node.get("probe_status") == "not_checked" for node in stored))
|
self.assertEqual(7, sum(node.get("probe_status") == "not_checked" for node in stored))
|
||||||
|
|
||||||
|
def test_ip_classification_separates_proxy_use_from_network_type(self) -> None:
|
||||||
|
residential, residential_reason = manager.vpn_utils.classify_ip_type(
|
||||||
|
{
|
||||||
|
"isp": "Sony Network Communications Inc.",
|
||||||
|
"org": "Sony Network Communications Inc.",
|
||||||
|
"proxy": True,
|
||||||
|
"hosting": False,
|
||||||
|
"mobile": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
softether, softether_reason = manager.vpn_utils.classify_ip_type(
|
||||||
|
{
|
||||||
|
"isp": "SoftEther",
|
||||||
|
"org": "SoftEther Corporation",
|
||||||
|
"proxy": True,
|
||||||
|
"hosting": False,
|
||||||
|
"mobile": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
hosting, hosting_reason = manager.vpn_utils.classify_ip_type(
|
||||||
|
{"proxy": True, "hosting": True, "mobile": False}
|
||||||
|
)
|
||||||
|
mobile, mobile_reason = manager.vpn_utils.classify_ip_type(
|
||||||
|
{"proxy": False, "hosting": False, "mobile": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(("residential", "consumer_or_unclassified_network"), (residential, residential_reason))
|
||||||
|
self.assertEqual(("hosting", "proxy_provider_datacenter"), (softether, softether_reason))
|
||||||
|
self.assertEqual(("hosting", "hosting_flag"), (hosting, hosting_reason))
|
||||||
|
self.assertEqual(("mobile", "mobile_flag"), (mobile, mobile_reason))
|
||||||
|
|
||||||
|
def test_ip_enrichment_reclassifies_legacy_cache_and_keeps_proxy_quality(self) -> None:
|
||||||
|
ip = "118.240.250.95"
|
||||||
|
manager.vpn_utils.IP_CACHE_FILE.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
ip: {
|
||||||
|
"ip_type": "hosting",
|
||||||
|
"quality": "proxy",
|
||||||
|
"cached_at": 9999999999,
|
||||||
|
"classification_version": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
api_result = [
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"query": ip,
|
||||||
|
"country": "Japan",
|
||||||
|
"regionName": "Tokyo",
|
||||||
|
"city": "Tokyo",
|
||||||
|
"isp": "Sony Network Communications Inc.",
|
||||||
|
"org": "Sony Network Communications Inc.",
|
||||||
|
"as": "AS2527 Sony Network Communications Inc.",
|
||||||
|
"asname": "Sony Network Communications Inc.",
|
||||||
|
"proxy": True,
|
||||||
|
"hosting": False,
|
||||||
|
"mobile": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
response = mock.MagicMock()
|
||||||
|
response.read.return_value = json.dumps(api_result).encode("utf-8")
|
||||||
|
response.__enter__.return_value = response
|
||||||
|
node = {"id": "sony", "ip": ip}
|
||||||
|
|
||||||
|
with mock.patch.object(manager.vpn_utils.urllib.request, "urlopen", return_value=response) as urlopen_mock:
|
||||||
|
manager.vpn_utils.enrich_ip_info([node])
|
||||||
|
|
||||||
|
self.assertEqual("residential", node["ip_type"])
|
||||||
|
self.assertEqual("proxy", node["quality"])
|
||||||
|
self.assertTrue(node["is_proxy"])
|
||||||
|
self.assertFalse(node["is_hosting"])
|
||||||
|
urlopen_mock.assert_called_once()
|
||||||
|
cache = json.loads(manager.vpn_utils.IP_CACHE_FILE.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(manager.vpn_utils.IP_CLASSIFICATION_VERSION, cache[ip]["classification_version"])
|
||||||
|
|
||||||
|
def test_background_ip_enrichment_merges_metadata_without_replacing_status(self) -> None:
|
||||||
|
nodes = self.write_nodes(2)
|
||||||
|
nodes[0]["probe_status"] = "available"
|
||||||
|
manager.write_json(manager.NODES_FILE, nodes)
|
||||||
|
|
||||||
|
def fake_enrich(items):
|
||||||
|
for item in items:
|
||||||
|
item["ip_type"] = "residential"
|
||||||
|
item["quality"] = "proxy"
|
||||||
|
item["owner"] = "Consumer ISP"
|
||||||
|
item["is_proxy"] = True
|
||||||
|
|
||||||
|
with mock.patch.object(manager.vpn_utils, "enrich_ip_info", side_effect=fake_enrich):
|
||||||
|
changed = manager.enrich_stored_nodes()
|
||||||
|
|
||||||
|
stored = manager.read_nodes()
|
||||||
|
self.assertGreater(changed, 0)
|
||||||
|
self.assertEqual("available", next(node for node in stored if node["id"] == "node-0")["probe_status"])
|
||||||
|
self.assertTrue(all(node["ip_type"] == "residential" for node in stored))
|
||||||
|
|
||||||
|
def test_source_deadline_skips_same_host_http_and_uses_github_https(self) -> None:
|
||||||
|
csv_text = valid_snapshot()
|
||||||
|
|
||||||
|
def fake_fetch(url, verify_ssl=True, deadline_seconds=None):
|
||||||
|
if url == manager.API_HTTPS_URL:
|
||||||
|
raise manager.SourceDeadlineExceeded("slow official source")
|
||||||
|
if url == manager.MIRROR_HTTPS_URL:
|
||||||
|
return csv_text
|
||||||
|
raise AssertionError(f"unexpected source: {url}")
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(manager, "fetch_api_text_with_deadline", side_effect=fake_fetch) as fetch_mock,
|
||||||
|
mock.patch.object(manager, "load_blacklist", return_value={}),
|
||||||
|
mock.patch.object(manager, "log_to_json"),
|
||||||
|
):
|
||||||
|
nodes = manager.fetch_candidates()
|
||||||
|
|
||||||
|
self.assertEqual(1, len(nodes))
|
||||||
|
self.assertEqual(
|
||||||
|
[manager.API_HTTPS_URL, manager.MIRROR_HTTPS_URL],
|
||||||
|
[call.args[0] for call in fetch_mock.call_args_list],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_source_deadline_limits_total_fetch_time(self) -> None:
|
||||||
|
def slow_fetch(url, verify_ssl=True):
|
||||||
|
threading.Event().wait(0.1)
|
||||||
|
return valid_snapshot()
|
||||||
|
|
||||||
|
with mock.patch.object(manager, "fetch_api_text", side_effect=slow_fetch):
|
||||||
|
started = manager.time.monotonic()
|
||||||
|
with self.assertRaises(manager.SourceDeadlineExceeded):
|
||||||
|
manager.fetch_api_text_with_deadline(
|
||||||
|
manager.API_HTTPS_URL,
|
||||||
|
deadline_seconds=0.01,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertLess(manager.time.monotonic() - started, 0.08)
|
||||||
|
|
||||||
def test_node_probe_stops_after_systemic_openvpn_failure(self) -> None:
|
def test_node_probe_stops_after_systemic_openvpn_failure(self) -> None:
|
||||||
nodes = self.write_nodes(12)
|
nodes = self.write_nodes(12)
|
||||||
|
|
||||||
@@ -394,9 +533,74 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
self.assertIn('class="country-option-input"', manager.INDEX_HTML)
|
self.assertIn('class="country-option-input"', manager.INDEX_HTML)
|
||||||
self.assertIn('${testBtn}', manager.INDEX_HTML)
|
self.assertIn('${testBtn}', manager.INDEX_HTML)
|
||||||
|
|
||||||
|
def test_web_dashboard_has_browser_freeze_safeguards(self) -> None:
|
||||||
|
self.assertNotIn("backdrop-filter", manager.LOGIN_HTML)
|
||||||
|
self.assertNotIn("backdrop-filter", manager.INDEX_HTML)
|
||||||
|
self.assertNotIn("background-attachment: fixed", manager.INDEX_HTML)
|
||||||
|
self.assertIn("@media (prefers-reduced-motion: reduce)", manager.LOGIN_HTML)
|
||||||
|
self.assertIn("@media (prefers-reduced-motion: reduce)", manager.INDEX_HTML)
|
||||||
|
self.assertIn("const pageSize = 50;", manager.INDEX_HTML)
|
||||||
|
self.assertIn('id="pagination_container"', manager.INDEX_HTML)
|
||||||
|
self.assertIn('paginationContainer.style.display = totalPages > 1 ? "flex" : "none";', manager.INDEX_HTML)
|
||||||
|
self.assertIn("const MAX_RENDERED_LOG_LINES = 300;", manager.INDEX_HTML)
|
||||||
|
self.assertIn("nodesRequestPromise", manager.INDEX_HTML)
|
||||||
|
self.assertIn("backgroundPollInFlight", manager.INDEX_HTML)
|
||||||
|
self.assertIn('let lastNodesSnapshotSignature = "";', manager.INDEX_HTML)
|
||||||
|
self.assertIn("if (signature === lastNodesSnapshotSignature) return false;", manager.INDEX_HTML)
|
||||||
|
self.assertIn('typeof document.hidden !== "boolean" || !document.hidden', manager.INDEX_HTML)
|
||||||
|
self.assertEqual(500, manager.WEB_LOG_MAX_ENTRIES)
|
||||||
|
|
||||||
|
def test_web_dashboard_has_cross_browser_interaction_safeguards(self) -> None:
|
||||||
|
self.assertNotIn("fonts.googleapis.com", manager.LOGIN_HTML)
|
||||||
|
self.assertNotIn("fonts.googleapis.com", manager.INDEX_HTML)
|
||||||
|
self.assertIn('const pwd = document.getElementById("password").value;', manager.LOGIN_HTML)
|
||||||
|
self.assertIn('const password = $("cred_password").value;', manager.INDEX_HTML)
|
||||||
|
self.assertIn("function fetchWithTimeout", manager.LOGIN_HTML)
|
||||||
|
self.assertIn("function fetchWithTimeout", manager.INDEX_HTML)
|
||||||
|
self.assertNotIn("await fetch(", manager.INDEX_HTML)
|
||||||
|
self.assertIn('role="dialog" aria-modal="true"', manager.INDEX_HTML)
|
||||||
|
self.assertIn('aria-label="关闭网页安全设置"', manager.INDEX_HTML)
|
||||||
|
self.assertIn('class="option-card active" data-value="auto" aria-pressed="true"', manager.INDEX_HTML)
|
||||||
|
self.assertIn('class="vps-recommend-tab"', manager.INDEX_HTML)
|
||||||
|
self.assertIn('position: static;', manager.INDEX_HTML)
|
||||||
|
self.assertIn('-webkit-overflow-scrolling: touch;', manager.INDEX_HTML)
|
||||||
|
self.assertIn('formatUrlHost(window.location.hostname)', manager.INDEX_HTML)
|
||||||
|
self.assertNotIn('id="status" class="status" style="display: none;"', manager.INDEX_HTML)
|
||||||
|
self.assertIn('${esc(localProxy)}', manager.INDEX_HTML)
|
||||||
|
self.assertIn('${esc(statusMessage)}', manager.INDEX_HTML)
|
||||||
|
|
||||||
|
def test_random_password_uses_cryptographic_randomness(self) -> None:
|
||||||
|
with mock.patch.object(manager.secrets, "choice", side_effect=list("aA0aA0aA0aA0")) as choice:
|
||||||
|
password = manager.generate_random_password()
|
||||||
|
|
||||||
|
self.assertEqual("aA0aA0aA0aA0", password)
|
||||||
|
self.assertEqual(12, choice.call_count)
|
||||||
|
|
||||||
|
def test_expired_sessions_are_removed(self) -> None:
|
||||||
|
manager.active_sessions.update({"expired": 99.0, "active": 101.0})
|
||||||
|
|
||||||
|
removed = manager.purge_expired_sessions(now=100.0)
|
||||||
|
|
||||||
|
self.assertEqual(1, removed)
|
||||||
|
self.assertEqual({"active": 101.0}, manager.active_sessions)
|
||||||
|
|
||||||
|
def test_web_log_reader_only_returns_recent_valid_entries(self) -> None:
|
||||||
|
log_file = manager.DATA_DIR / "logs" / "current.json"
|
||||||
|
log_file.parent.mkdir(parents=True)
|
||||||
|
with log_file.open("w", encoding="utf-8") as f:
|
||||||
|
for index in range(520):
|
||||||
|
f.write(json.dumps({"index": index}) + "\n")
|
||||||
|
f.write("not-json\n")
|
||||||
|
|
||||||
|
entries = manager.read_recent_log_entries(log_file)
|
||||||
|
|
||||||
|
self.assertEqual(500, len(entries))
|
||||||
|
self.assertEqual(20, entries[0]["index"])
|
||||||
|
self.assertEqual(519, entries[-1]["index"])
|
||||||
|
|
||||||
def test_web_update_controls_only_expose_stable_main_channel(self) -> None:
|
def test_web_update_controls_only_expose_stable_main_channel(self) -> None:
|
||||||
self.assertEqual("2.1.0", manager.APP_VERSION)
|
self.assertEqual("2.1.2", manager.APP_VERSION)
|
||||||
self.assertEqual("V2.1 正式版", manager.APP_VERSION_LABEL)
|
self.assertEqual("V2.1.2 正式版", manager.APP_VERSION_LABEL)
|
||||||
self.assertIn("检测更新", manager.INDEX_HTML)
|
self.assertIn("检测更新", manager.INDEX_HTML)
|
||||||
self.assertIn("/api/check_update", manager.INDEX_HTML)
|
self.assertIn("/api/check_update", manager.INDEX_HTML)
|
||||||
self.assertIn("/tree/main", manager.INDEX_HTML)
|
self.assertIn("/tree/main", manager.INDEX_HTML)
|
||||||
@@ -414,6 +618,26 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
self.assertNotIn("origin/master", install_text)
|
self.assertNotIn("origin/master", install_text)
|
||||||
self.assertNotIn("bate", install_text.lower())
|
self.assertNotIn("bate", install_text.lower())
|
||||||
|
|
||||||
|
def test_installer_uses_secure_credentials_and_current_version(self) -> None:
|
||||||
|
install_text = (manager.ROOT_DIR / "install.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertNotIn("random.choices", install_text)
|
||||||
|
self.assertIn("secrets.choice", install_text)
|
||||||
|
self.assertIn('get_app_version()', install_text)
|
||||||
|
self.assertNotIn("管理终端 v2.0", install_text)
|
||||||
|
self.assertIn("5-90 秒", install_text)
|
||||||
|
self.assertIn('new_pwd = input("请输入新管理密码 (不能为空): ")', install_text)
|
||||||
|
self.assertIn('state["active_openvpn_node_id"] = ""', install_text)
|
||||||
|
self.assertIn("ip link show dev tun0", install_text)
|
||||||
|
self.assertIn("pidof openvpn", install_text)
|
||||||
|
|
||||||
|
def test_release_workflow_uses_full_patch_version(self) -> None:
|
||||||
|
workflow_text = (manager.ROOT_DIR / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn("default: v2.1.2", workflow_text)
|
||||||
|
self.assertIn("AimiliVPN V$(tr -d '\\r\\n' < VERSION) 正式版", workflow_text)
|
||||||
|
self.assertNotIn("cut -d. -f1,2 VERSION", workflow_text)
|
||||||
|
|
||||||
def test_latest_release_check_ignores_non_version_name_text(self) -> None:
|
def test_latest_release_check_ignores_non_version_name_text(self) -> None:
|
||||||
release = {
|
release = {
|
||||||
"tag_name": "v2.2.0",
|
"tag_name": "v2.2.0",
|
||||||
@@ -437,8 +661,8 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_latest_release_check_reports_current_formal_version(self) -> None:
|
def test_latest_release_check_reports_current_formal_version(self) -> None:
|
||||||
release = {
|
release = {
|
||||||
"tag_name": "v2.1.0",
|
"tag_name": "v2.1.2",
|
||||||
"name": "AimiliVPN V2.1 正式版",
|
"name": "AimiliVPN V2.1.2 正式版",
|
||||||
"draft": False,
|
"draft": False,
|
||||||
"prerelease": False,
|
"prerelease": False,
|
||||||
}
|
}
|
||||||
@@ -446,7 +670,7 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
result = manager.check_latest_release()
|
result = manager.check_latest_release()
|
||||||
|
|
||||||
self.assertFalse(result["update_available"])
|
self.assertFalse(result["update_available"])
|
||||||
self.assertEqual("V2.1 正式版", result["current_version_label"])
|
self.assertEqual("V2.1.2 正式版", result["current_version_label"])
|
||||||
|
|
||||||
def test_latest_release_check_reports_source_update_command(self) -> None:
|
def test_latest_release_check_reports_source_update_command(self) -> None:
|
||||||
release = {"tag_name": "v2.2.0", "draft": False, "prerelease": False}
|
release = {"tag_name": "v2.2.0", "draft": False, "prerelease": False}
|
||||||
@@ -570,6 +794,31 @@ class ManagerLogicTests(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class ProxyServerConcurrencyTests(unittest.TestCase):
|
class ProxyServerConcurrencyTests(unittest.TestCase):
|
||||||
|
def test_socks5_rejects_client_without_no_auth_method(self) -> None:
|
||||||
|
class Client:
|
||||||
|
def __init__(self):
|
||||||
|
self.incoming = bytearray(b"\x01\x02")
|
||||||
|
self.sent = bytearray()
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
def recv(self, size):
|
||||||
|
chunk = self.incoming[:size]
|
||||||
|
del self.incoming[:size]
|
||||||
|
return bytes(chunk)
|
||||||
|
|
||||||
|
def sendall(self, data):
|
||||||
|
self.sent.extend(data)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
with mock.patch.object(proxy_server, "proxy_auth_enabled", return_value=False):
|
||||||
|
proxy_server.socks5_client(client, b"\x05")
|
||||||
|
|
||||||
|
self.assertEqual(b"\x05\xff", bytes(client.sent))
|
||||||
|
self.assertTrue(client.closed)
|
||||||
|
|
||||||
def test_each_proxy_worker_keeps_its_accepted_socket(self) -> None:
|
def test_each_proxy_worker_keeps_its_accepted_socket(self) -> None:
|
||||||
class Client:
|
class Client:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
|
|||||||
+62
-23
@@ -15,11 +15,18 @@ from typing import Any
|
|||||||
ROOT_DIR = Path(__file__).resolve().parent
|
ROOT_DIR = Path(__file__).resolve().parent
|
||||||
DATA_DIR = Path(os.environ["VPNGATE_DATA_DIR"]).resolve() if os.environ.get("VPNGATE_DATA_DIR") else ROOT_DIR / "vpngate_data"
|
DATA_DIR = Path(os.environ["VPNGATE_DATA_DIR"]).resolve() if os.environ.get("VPNGATE_DATA_DIR") else ROOT_DIR / "vpngate_data"
|
||||||
IP_CACHE_FILE = DATA_DIR / "ip_cache.json"
|
IP_CACHE_FILE = DATA_DIR / "ip_cache.json"
|
||||||
|
IP_CLASSIFICATION_VERSION = 2
|
||||||
|
IP_CACHE_TTL_SECONDS = 7 * 24 * 3600
|
||||||
|
|
||||||
ip_cache_lock = threading.RLock()
|
ip_cache_lock = threading.RLock()
|
||||||
physical_interface_lock = threading.Lock()
|
physical_interface_lock = threading.Lock()
|
||||||
physical_interface_cache: tuple[str | None, float] = (None, 0.0)
|
physical_interface_cache: tuple[str | None, float] = (None, 0.0)
|
||||||
|
|
||||||
|
DATACENTER_PROVIDER_PATTERN = re.compile(
|
||||||
|
r"(?:\b(?:cloud|colo|colocation|data[ -]?center|hosting|servers?|vps)\b|softether)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
COUNTRY_TRANSLATIONS = {
|
COUNTRY_TRANSLATIONS = {
|
||||||
"Japan": "日本",
|
"Japan": "日本",
|
||||||
"Korea Republic of": "韩国",
|
"Korea Republic of": "韩国",
|
||||||
@@ -389,6 +396,39 @@ def save_ip_cache(cache: dict[str, dict[str, Any]]) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def classify_ip_type(item: dict[str, Any]) -> tuple[str, str]:
|
||||||
|
"""Classify network ownership without confusing VPN use with hosting."""
|
||||||
|
if item.get("mobile"):
|
||||||
|
return "mobile", "mobile_flag"
|
||||||
|
if item.get("hosting"):
|
||||||
|
return "hosting", "hosting_flag"
|
||||||
|
|
||||||
|
provider_text = " ".join(
|
||||||
|
str(item.get(key) or "")
|
||||||
|
for key in ("isp", "org", "as", "asname")
|
||||||
|
)
|
||||||
|
if item.get("proxy") and DATACENTER_PROVIDER_PATTERN.search(provider_text):
|
||||||
|
return "hosting", "proxy_provider_datacenter"
|
||||||
|
|
||||||
|
# A residential volunteer running VPNGate is commonly marked as a proxy.
|
||||||
|
# Proxy use is retained in quality/is_proxy and must not change ownership.
|
||||||
|
return "residential", "consumer_or_unclassified_network"
|
||||||
|
|
||||||
|
def apply_ip_cache_entry(node: dict[str, Any], entry: dict[str, Any]) -> None:
|
||||||
|
for key in (
|
||||||
|
"owner",
|
||||||
|
"asn",
|
||||||
|
"as_name",
|
||||||
|
"location",
|
||||||
|
"ip_type",
|
||||||
|
"quality",
|
||||||
|
"is_proxy",
|
||||||
|
"is_hosting",
|
||||||
|
"is_mobile",
|
||||||
|
"ip_type_reason",
|
||||||
|
):
|
||||||
|
node[key] = entry.get(key, "")
|
||||||
|
|
||||||
def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
||||||
# 1. Read cache thread-safely
|
# 1. Read cache thread-safely
|
||||||
with ip_cache_lock:
|
with ip_cache_lock:
|
||||||
@@ -401,14 +441,14 @@ def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
|||||||
ip = node.get("ip") or node.get("remote_host")
|
ip = node.get("ip") or node.get("remote_host")
|
||||||
if not ip:
|
if not ip:
|
||||||
continue
|
continue
|
||||||
if ip in cache and now - cache[ip].get("cached_at", 0) < 7 * 24 * 3600:
|
cache_entry = cache.get(ip) if isinstance(cache.get(ip), dict) else None
|
||||||
|
if (
|
||||||
|
cache_entry
|
||||||
|
and cache_entry.get("classification_version") == IP_CLASSIFICATION_VERSION
|
||||||
|
and now - cache_entry.get("cached_at", 0) < IP_CACHE_TTL_SECONDS
|
||||||
|
):
|
||||||
cached = cache[ip]
|
cached = cache[ip]
|
||||||
node["owner"] = cached.get("owner", "")
|
apply_ip_cache_entry(node, cached)
|
||||||
node["asn"] = cached.get("asn", "")
|
|
||||||
node["as_name"] = cached.get("as_name", "")
|
|
||||||
node["location"] = cached.get("location", "")
|
|
||||||
node["ip_type"] = cached.get("ip_type", "")
|
|
||||||
node["quality"] = cached.get("quality", "")
|
|
||||||
else:
|
else:
|
||||||
if ip not in ips_to_query:
|
if ip not in ips_to_query:
|
||||||
ips_to_query.append(ip)
|
ips_to_query.append(ip)
|
||||||
@@ -425,7 +465,10 @@ def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
|||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
"http://ip-api.com/batch?lang=zh-CN&fields=status,message,query,country,regionName,city,isp,org,as,asname,proxy,hosting,mobile",
|
"http://ip-api.com/batch?lang=zh-CN&fields=status,message,query,country,regionName,city,isp,org,as,asname,proxy,hosting,mobile",
|
||||||
data=payload,
|
data=payload,
|
||||||
headers={"Content-Type": "application/json", "User-Agent": "vpngate-manager/2.2"},
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": f"AimiliVPN-IP-Classifier/{IP_CLASSIFICATION_VERSION}",
|
||||||
|
},
|
||||||
method="POST",
|
method="POST",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -442,19 +485,15 @@ def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
|||||||
if not query_ip:
|
if not query_ip:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ip_type = "residential"
|
ip_type, ip_type_reason = classify_ip_type(item)
|
||||||
if item.get("mobile"):
|
|
||||||
ip_type = "mobile"
|
|
||||||
elif item.get("hosting") or item.get("proxy"):
|
|
||||||
ip_type = "hosting"
|
|
||||||
|
|
||||||
quality = "normal"
|
quality = "normal"
|
||||||
if item.get("proxy"):
|
if item.get("mobile"):
|
||||||
quality = "proxy"
|
quality = "mobile"
|
||||||
elif item.get("hosting"):
|
elif item.get("hosting"):
|
||||||
quality = "datacenter"
|
quality = "datacenter"
|
||||||
elif item.get("mobile"):
|
elif item.get("proxy"):
|
||||||
quality = "mobile"
|
quality = "proxy"
|
||||||
|
|
||||||
loc = " ".join(part for part in [item.get("country"), item.get("regionName"), item.get("city")] if part)
|
loc = " ".join(part for part in [item.get("country"), item.get("regionName"), item.get("city")] if part)
|
||||||
|
|
||||||
@@ -465,6 +504,11 @@ def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
|||||||
"location": loc,
|
"location": loc,
|
||||||
"ip_type": ip_type,
|
"ip_type": ip_type,
|
||||||
"quality": quality,
|
"quality": quality,
|
||||||
|
"is_proxy": bool(item.get("proxy")),
|
||||||
|
"is_hosting": bool(item.get("hosting")),
|
||||||
|
"is_mobile": bool(item.get("mobile")),
|
||||||
|
"ip_type_reason": ip_type_reason,
|
||||||
|
"classification_version": IP_CLASSIFICATION_VERSION,
|
||||||
"cached_at": now,
|
"cached_at": now,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -484,12 +528,7 @@ def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
|||||||
ip = node.get("ip") or node.get("remote_host")
|
ip = node.get("ip") or node.get("remote_host")
|
||||||
if ip in new_entries:
|
if ip in new_entries:
|
||||||
cached = new_entries[ip]
|
cached = new_entries[ip]
|
||||||
node["owner"] = cached.get("owner", "")
|
apply_ip_cache_entry(node, cached)
|
||||||
node["asn"] = cached.get("asn", "")
|
|
||||||
node["as_name"] = cached.get("as_name", "")
|
|
||||||
node["location"] = cached.get("location", "")
|
|
||||||
node["ip_type"] = cached.get("ip_type", "")
|
|
||||||
node["quality"] = cached.get("quality", "")
|
|
||||||
|
|
||||||
|
|
||||||
def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -> tuple[int, str]:
|
def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -> tuple[int, str]:
|
||||||
|
|||||||
+541
-205
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user