Refresh Harbor docs and subscription controls
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 1s

This commit is contained in:
2026-07-11 16:42:03 +03:00
parent b53cd08dcc
commit 0bf7d2ee30
5 changed files with 316 additions and 81 deletions

286
README.md
View File

@@ -1,47 +1,297 @@
# Harbor
Один компактный VPN-продукт в двух режимах:
Harbor помогает пользоваться одной VPN-подпиской дома и на Mac без ручной настройки `sing-box`.
- **Harbor Gateway** — отдельная Linux-машина принимает трафик устройств как системный Gateway или Gateway HTTP/SOCKS5 Proxy;
- **Harbor Connect** — локальный proxy-клиент для macOS.
Проект работает в двух режимах:
В обоих режимах пользователь добавляет подписку, выбирает сервер и включает VPN на одном экране.
| Режим | Где работает | Для чего нужен |
| --- | --- | --- |
| **Harbor Gateway** | На отдельной Linux-машине | Проводит через VPN весь интернет-трафик домашних устройств или работает как общий HTTP/SOCKS5-прокси |
| **Harbor Connect** | На macOS | Даёт приложениям на Mac локальный HTTP/SOCKS5-прокси |
## Gateway
В обоих режимах управление одинаковое: откройте веб-интерфейс, вставьте ссылку VPN-подписки, выберите сервер и нажмите кнопку подключения.
## Что понадобится
- ссылка на подписку от VPN-провайдера;
- Git;
- Docker с командой `docker compose`;
- для Gateway — Linux-машина в одной локальной сети с устройствами;
- для Connect — Mac с запущенным Docker Desktop.
Harbor не является VPN-провайдером и не создаёт подписки самостоятельно.
## Что выбрать
Используйте **Harbor Connect**, если VPN нужен только приложениям на одном Mac.
Используйте **Harbor Gateway**, если нужно подключить телевизор, телефон, игровую приставку или сразу несколько устройств. Устройства можно направить через Gateway целиком либо настроить в отдельных приложениях общий прокси.
Оба режима можно использовать вместе. Дома Connect автоматически распознаёт настроенный Harbor Gateway и не запускает второй VPN-маршрут. В другой сети Connect возвращается к локальному VPN.
## Установка Harbor Gateway
### 1. Скачайте проект
```bash
git clone https://git.dokops.ru/dokril/vpn-proxy.git
cd vpn-proxy
```
### 2. Создайте настройки
```bash
cp .env.example .env
```
Стандартные значения подходят для обычной домашней сети. При необходимости откройте `.env` в текстовом редакторе и измените порты.
### 3. Запустите Gateway
```bash
docker compose -f docker-compose.gateway.yml up -d --build
```
Интерфейс: `http://<gateway-ip>:3456`.
Откройте в браузере:
После подключения экран показывает:
```text
http://АДРЕС-GATEWAY:3456
```
- `Gateway` — адрес, который можно назначить устройству как основной шлюз;
- `Gateway Proxy` — один адрес на порту `8080`, доступный как `HTTP` и `SOCKS5`.
Например, если Linux-машина имеет адрес `192.168.1.20`, интерфейс будет доступен по адресу `http://192.168.1.20:3456`.
Когда VPN включён, публичный трафик Gateway и Gateway Proxy идёт через выбранный sing-box outbound. Когда VPN выключен, TProxy-перехват снимается и Gateway продолжает работать напрямую через kernel forwarding/NAT без прохода через sing-box.
### 4. Добавьте подписку
Proxy по умолчанию разрешён только из приватных сетей. Диапазоны задаются через `PROXY_ALLOWED_CIDRS`.
1. Вставьте ссылку VPN-подписки.
2. Нажмите «Сохранить подписку».
3. Выберите сервер.
4. Включите VPN.
## macOS client
После подключения Harbor покажет два варианта использования:
- **Gateway** — укажите IP-адрес Linux-машины как основной шлюз устройства. Через VPN пойдёт весь его интернет-трафик;
- **Gateway Proxy** — укажите адрес Linux-машины и порт `8080` в приложении. Поддерживаются HTTP и SOCKS5 на одном порту.
Приватные и локальные адреса не отправляются в VPN, поэтому устройства сохраняют доступ к домашней сети. Общий прокси по умолчанию принимает подключения только из приватных сетей.
## Установка Harbor Connect на macOS
### 1. Запустите Docker Desktop
Установщик проверит наличие Git, Docker, Docker Compose и `curl`. Если Docker Desktop не запущен, установка остановится с понятным сообщением.
### 2. Скачайте проект и запустите установщик
```bash
git clone https://git.dokops.ru/dokril/vpn-proxy.git
cd vpn-proxy
./scripts/install-macos-client.sh
```
По умолчанию интерфейс доступен на `http://127.0.0.1:3456`, локальный HTTP/SOCKS5 proxy — на `127.0.0.1:8082`.
Установщик:
Установщик также добавляет пользовательский LaunchAgent. Он раз в 5 секунд передаёт Connect текущий default gateway macOS. Если этот адрес отвечает как Harbor Gateway и подтверждает ту же подписку, Connect автоматически оставляет локальный proxy на `127.0.0.1`, но переключает его outbound на `direct`: дальнейший трафик обрабатывает системный Harbor Gateway. При смене сети или трёх ошибках проверки Connect возвращается к выбранному локальному VPN.
- сохранит рабочую копию в `~/.vpn-proxy-client`;
- предложит порт для локального прокси;
- соберёт и запустит контейнер Harbor Connect;
- добавит пользовательский LaunchAgent для определения текущего Gateway.
Автоопределение не требует выбора домашней Wi-Fi сети. На Connect и Gateway должна быть настроена ссылка с одним и тем же секретным token длиной не менее 24 символов; token используется только для проверки presence-ответа и не передаётся между устройствами.
По умолчанию используются адреса:
## Проверка
| Назначение | Адрес |
| --- | --- |
| Интерфейс Harbor Connect | `http://127.0.0.1:3456` |
| HTTP-прокси | `127.0.0.1:8082` |
| SOCKS5-прокси | `127.0.0.1:8082` |
### 3. Добавьте подписку
Откройте `http://127.0.0.1:3456`, вставьте ссылку подписки, выберите сервер и включите VPN.
Сам по себе локальный прокси не перенаправляет приложения автоматически. Адрес `127.0.0.1:8082` нужно указать в настройках нужного приложения или в системных настройках macOS.
### Другие порты
Передайте нужные значения при повторном запуске установщика:
```bash
VPN_PROXY_CLIENT_PORT=9080 \
VPN_PROXY_CLIENT_UI_PORT=3457 \
./scripts/install-macos-client.sh
```
Допустимы порты от `1024` до `65535`. Установщик не позволит выбрать занятый порт или один порт одновременно для интерфейса и прокси.
## Системный прокси macOS
Сначала посмотрите точное имя сетевого подключения:
```bash
networksetup -listallnetworkservices
```
Для подключения с именем `Wi-Fi` включите HTTP, HTTPS и SOCKS5-прокси:
```bash
networksetup -setwebproxy Wi-Fi 127.0.0.1 8082
networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 8082
networksetup -setsocksfirewallproxy Wi-Fi 127.0.0.1 8082
```
Чтобы отключить их:
```bash
networksetup -setwebproxystate Wi-Fi off
networksetup -setsecurewebproxystate Wi-Fi off
networksetup -setsocksfirewallproxystate Wi-Fi off
```
Если сетевое подключение называется иначе, замените `Wi-Fi` его точным именем.
## Автоматическое использование домашнего Gateway
Harbor Connect раз в пять секунд узнаёт у macOS адрес текущего основного шлюза. Если по этому адресу работает Harbor Gateway с той же VPN-подпиской, Connect оставляет локальный прокси доступным для приложений, но не создаёт второй VPN-маршрут: трафик уже обрабатывает Gateway.
Для этого:
1. добавьте одну и ту же ссылку подписки в Gateway и Connect;
2. убедитесь, что Mac может открыть интерфейс Gateway на порту `3456`;
3. оставьте автоматический режим включённым в Harbor Connect.
Ссылка должна содержать персональный секрет или token длиной не менее 16 символов — обычные ссылки подписок уже соответствуют этому условию. Ссылка между устройствами не передаётся: она используется локально для проверки, что Connect нашёл именно ваш Gateway. При смене сети или после трёх неудачных проверок Connect возвращается к локальному VPN.
## Повседневные команды
Все команды Gateway выполняются из каталога проекта. Команды Connect — из `~/.vpn-proxy-client`.
### Harbor Gateway
| Действие | Команда |
| --- | --- |
| Запустить или обновить после изменения файлов | `docker compose -f docker-compose.gateway.yml up -d --build` |
| Показать состояние | `docker compose -f docker-compose.gateway.yml ps` |
| Смотреть журнал | `docker compose -f docker-compose.gateway.yml logs -f` |
| Перезапустить | `docker compose -f docker-compose.gateway.yml restart` |
| Остановить | `docker compose -f docker-compose.gateway.yml down` |
| Удалить вместе с сохранёнными данными | `docker compose -f docker-compose.gateway.yml down -v` |
### Harbor Connect
```bash
cd ~/.vpn-proxy-client
```
| Действие | Команда |
| --- | --- |
| Обновить и снова запустить | `./scripts/install-macos-client.sh` |
| Показать состояние | `docker compose -f docker-compose.client.yml ps` |
| Смотреть журнал | `docker compose -f docker-compose.client.yml logs -f` |
| Перезапустить | `docker compose -f docker-compose.client.yml restart` |
| Остановить | `docker compose -f docker-compose.client.yml down` |
| Удалить вместе с сохранёнными данными | `docker compose -f docker-compose.client.yml down -v` |
Команда с `-v` удаляет подписку, выбранный сервер и другие сохранённые данные. Для обычной остановки используйте `down` без `-v`.
## Обновление
### Gateway
```bash
git pull --ff-only
docker compose -f docker-compose.gateway.yml up -d --build
```
### Connect
Повторно запустите установщик. Он обновит рабочую копию, снова спросит порт прокси и пересоберёт Connect:
```bash
cd ~/.vpn-proxy-client
./scripts/install-macos-client.sh
```
Если раньше использовался нестандартный порт, укажите его снова через `VPN_PROXY_CLIENT_PORT`.
## Настройки `.env`
Для большинства установок достаточно стандартных значений.
| Переменная | По умолчанию | Назначение |
| --- | --- | --- |
| `PORT` | `3456` | Внутренний порт веб-интерфейса Gateway |
| `CLIENT_UI_PORT` | `3456` | Порт интерфейса Connect на Mac |
| `PROXY_PORT` | `8080` | Порт общего прокси Gateway |
| `CLIENT_PROXY_PORT` | `8082` | Порт локального прокси Connect |
| `HARBOR_GATEWAY_CONTROL_PORT` | `3456` | Порт, на котором Connect проверяет домашний Gateway |
| `PROXY_BIND_IP` | `0.0.0.0` | Адрес, на котором Gateway принимает прокси-подключения |
| `PROXY_ALLOWED_CIDRS` | приватные IPv4-сети | Сети, которым разрешён доступ к Gateway Proxy |
| `GATEWAY_CLIENT_CIDRS` | приватные IPv4-сети | Сети, трафик которых Gateway может маршрутизировать |
| `LOG_LEVEL` | `info` | Уровень подробности журнала |
Остальные значения в `.env.example` относятся к сборке контейнера и внутренней маршрутизации. Меняйте их только при нестандартном развёртывании.
После изменения `.env` пересоздайте контейнер командой `up -d` — обычного `restart` недостаточно.
## Если что-то не работает
### Интерфейс не открывается
Проверьте контейнер и журнал:
```bash
docker compose -f docker-compose.gateway.yml ps
docker compose -f docker-compose.gateway.yml logs --tail=100
```
Для Connect замените имя файла на `docker-compose.client.yml` и выполняйте команду из `~/.vpn-proxy-client`.
### Прокси не отвечает
Убедитесь, что Harbor включён в интерфейсе, а приложение использует правильные адрес и порт. Для Connect это обычно `127.0.0.1:8082`; для Gateway — IP Linux-машины и порт `8080`.
### Connect не распознаёт Gateway
Проверьте три условия:
- Gateway является текущим основным шлюзом Mac;
- на обоих устройствах сохранена одна и та же подписка;
- с Mac открывается `http://АДРЕС-GATEWAY:3456`.
### Проверка конфигурации без запуска
```bash
npm test
npm run build
docker compose -f docker-compose.gateway.yml config
docker compose -f docker-compose.client.yml config
```
Эти команды только проверяют и показывают итоговую конфигурацию Docker Compose.
## Служебные команды
Этот раздел нужен тем, кто собирает, проверяет или развёртывает сам проект. Для обычного использования он не требуется.
### Команды npm
| Команда | Назначение |
| --- | --- |
| `npm ci` | Установить точные версии зависимостей из `package-lock.json` |
| `npm test` | Запустить автоматические проверки |
| `npm run build` | Собрать веб-интерфейс в `dist/` |
| `npm run dev` | Запустить Vite для разработки интерфейса |
| `npm start` | Запустить управляющий Node.js-сервис в подготовленном окружении |
### Сборка и развёртывание
| Команда | Назначение |
| --- | --- |
| `./scripts/build-runtime-base.sh` | Собрать базовый runtime-образ с Node.js, сетевыми утилитами и `sing-box` |
| `./scripts/build-on-107-deploy-111.sh` | Собрать Gateway на хосте `107` и развернуть на хосте `111`; хосты меняются через `BUILD_HOST` и `DEPLOY_HOST` |
| `GATEWAY_IMAGE=<образ> ./scripts/deploy-gateway.sh` | Развернуть уже собранный образ в `/opt/vpn-proxy` |
| `./scripts/harbor-network-monitor.sh` | Один раз записать текущий Gateway macOS; обычно этот скрипт запускает установленный LaunchAgent |
Отправка изменений в ветку `master` также запускает автоматическую сборку и развёртывание Gateway через Gitea Actions.
## Хранение данных
Подписка, выбранный сервер и состояние подключения хранятся в именованных Docker volumes. Поэтому обычные команды `restart`, `down`, обновление проекта и повторная сборка не удаляют настройки.
Не публикуйте файл `.env`, ссылку подписки и содержимое Docker volumes. `.env` уже исключён из Git.

View File

@@ -1,22 +0,0 @@
services:
vpn-proxy-gateway:
image: ${GATEWAY_IMAGE}
container_name: vpn-proxy-gateway
network_mode: host
cap_add:
- NET_ADMIN
- NET_RAW
env_file:
- .env
environment:
DATA_DIR: /var/lib/vpn-proxy
SING_BOX_CONFIG: /etc/sing-box/config.json
SING_BOX_CACHE: /var/lib/sing-box/cache.db
volumes:
- vpn-proxy-data:/var/lib/vpn-proxy
- sing-box-cache:/var/lib/sing-box
restart: unless-stopped
volumes:
vpn-proxy-data:
sing-box-cache:

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="9" fill="#101812"/>
<circle cx="16" cy="16" r="11" fill="#65d889" opacity=".08"/>
<g fill="none" stroke="#78e29a" stroke-width="2.6" stroke-linecap="round">
<path d="M16 6.5v9"/>
<path d="M10.1 10.3a8 8 0 1 0 11.8 0"/>
</g>
<circle cx="16" cy="16" r="8.8" fill="none" stroke="#78e29a" opacity=".16"/>
</svg>

Before

Width:  |  Height:  |  Size: 420 B

View File

@@ -679,32 +679,36 @@ export function ClientOverviewPage({
inert={editingSubscription ? true : undefined}
>
<div className="client-subscription-heading">
<span>Ваша подписка</span>
<button
className="client-subscription-refresh client-tooltip-anchor"
type="button"
aria-label="Обновить подписку"
disabled={refreshingInfo}
onClick={refreshSubscription}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M21 12a9 9 0 0 0-15.2-6.5L3 8m0-5v5h5M3 12a9 9 0 0 0 15.2 6.5L21 16m0 5v-5h-5" />
</svg>
<span className="client-subscription-label">Ваша подписка</span>
<span className="client-icon-tooltip client-tooltip-anchor">
<button
className="client-subscription-refresh"
type="button"
aria-label="Обновить подписку"
disabled={refreshingInfo}
onClick={refreshSubscription}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M21 12a9 9 0 0 0-15.2-6.5L3 8m0-5v5h5M3 12a9 9 0 0 0 15.2 6.5L21 16m0 5v-5h-5" />
</svg>
</button>
<CloudTooltip>Обновить подписку</CloudTooltip>
</button>
<button
className="client-subscription-delete client-tooltip-anchor"
type="button"
aria-label="Удалить подписку"
disabled={busy}
onClick={() => setConfirmingDelete(true)}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path className="client-trash-lid" d="M4 7h16M9 7V4h6v3" />
<path d="m6 7 1 13h10l1-13M10 11v5M14 11v5" />
</svg>
</span>
<span className="client-icon-tooltip client-tooltip-anchor">
<button
className="client-subscription-delete"
type="button"
aria-label="Удалить подписку"
disabled={busy}
onClick={() => setConfirmingDelete(true)}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path className="client-trash-lid" d="M4 7h16M9 7V4h6v3" />
<path d="m6 7 1 13h10l1-13M10 11v5M14 11v5" />
</svg>
</button>
<CloudTooltip>Удалить подписку</CloudTooltip>
</button>
</span>
</div>
<button
className="client-subscription-domain-button"

View File

@@ -1046,15 +1046,15 @@ p {
.client-tooltip {
position: absolute;
top: calc(100% + 6px);
bottom: calc(100% + 6px);
left: 50%;
z-index: 12;
z-index: 50;
width: max-content;
max-width: 220px;
padding: 6px 8px;
border-radius: 8px;
background: color-mix(in oklch, var(--client-panel) 68%, transparent);
box-shadow: 0 7px 22px oklch(0.08 0.015 145 / 0.1);
background: color-mix(in oklch, var(--client-panel) 78%, transparent);
box-shadow: 0 8px 26px oklch(0.08 0.015 145 / 0.16);
backdrop-filter: blur(10px) saturate(0.9);
color: var(--client-text);
font: 600 10px/1.35 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
@@ -1065,12 +1065,12 @@ p {
visibility: hidden;
filter: blur(2px);
pointer-events: none;
transform: translate(-50%, -2px);
transform: translate(-50%, 2px);
transition: opacity 90ms ease, filter 120ms ease, transform 140ms cubic-bezier(0.16, 1, 0.3, 1), visibility 0s 140ms;
}
.client-tooltip-anchor:hover > .client-tooltip,
.client-tooltip-anchor:focus-visible > .client-tooltip {
.client-tooltip-anchor:focus-within > .client-tooltip {
opacity: 1;
visibility: visible;
filter: blur(0);
@@ -1078,6 +1078,13 @@ p {
transition-delay: 20ms, 20ms, 20ms, 0s;
}
.client-icon-tooltip {
width: 16px;
height: 16px;
display: grid;
place-items: center;
}
.client-power-section p {
min-height: 0;
line-height: 20px;
@@ -1285,6 +1292,11 @@ p {
pointer-events: none;
}
.client-subscription.is-editing .client-icon-tooltip {
opacity: 0;
pointer-events: none;
}
.client-subscription-edit,
.client-subscription-summary {
grid-area: 1 / 1;
@@ -1471,7 +1483,7 @@ p {
color: var(--client-muted);
text-align: center;
cursor: pointer;
overflow: hidden;
overflow: visible;
}
.client-subscription.is-editing .client-subscription-summary {
@@ -1481,7 +1493,7 @@ p {
pointer-events: none;
}
.client-subscription-heading > span {
.client-subscription-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;