mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
fixed server go
This commit is contained in:
@@ -214,6 +214,14 @@ if(AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY)
|
|||||||
target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY)
|
target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
option(AMNEZIA_LAN_PLAINTEXT_GATEWAY "Dev: plaintext JSON to private LAN gateway hosts (requires AMNEZIA_QR_PAIRING_ALLOW)" OFF)
|
||||||
|
if(AMNEZIA_LAN_PLAINTEXT_GATEWAY)
|
||||||
|
if(NOT AMNEZIA_QR_PAIRING_ALLOW)
|
||||||
|
message(FATAL_ERROR "AMNEZIA_LAN_PLAINTEXT_GATEWAY=ON requires AMNEZIA_QR_PAIRING_ALLOW=ON")
|
||||||
|
endif()
|
||||||
|
target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_LAN_PLAINTEXT_GATEWAY)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
||||||
|
|
||||||
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QSysInfo>
|
#include <QSysInfo>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
#include "core/controllers/gatewayController.h"
|
#include "core/controllers/gatewayController.h"
|
||||||
#include "core/repositories/secureAppSettingsRepository.h"
|
#include "core/repositories/secureAppSettingsRepository.h"
|
||||||
#include "core/utils/api/apiUtils.h"
|
#include "core/utils/api/apiUtils.h"
|
||||||
#include "core/utils/constants/apiConstants.h"
|
#include "core/utils/constants/apiConstants.h"
|
||||||
#include "core/utils/constants/apiKeys.h"
|
#include "core/utils/constants/apiKeys.h"
|
||||||
|
#include "core/utils/networkUtilities.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
using namespace amnezia;
|
using namespace amnezia;
|
||||||
@@ -22,10 +24,18 @@ constexpr qsizetype kPairingMaxApiKeyChars = 8192;
|
|||||||
|
|
||||||
bool isLocalGatewayHost(const QString &gatewayUrl)
|
bool isLocalGatewayHost(const QString &gatewayUrl)
|
||||||
{
|
{
|
||||||
return gatewayUrl.contains(QStringLiteral("127.0.0.1"), Qt::CaseInsensitive)
|
if (gatewayUrl.contains(QStringLiteral("127.0.0.1"), Qt::CaseInsensitive)
|
||||||
|| gatewayUrl.contains(QStringLiteral("localhost"), Qt::CaseInsensitive)
|
|| gatewayUrl.contains(QStringLiteral("localhost"), Qt::CaseInsensitive)
|
||||||
|| gatewayUrl.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)
|
|| gatewayUrl.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)
|
||||||
|| gatewayUrl.contains(QStringLiteral("::1"), Qt::CaseInsensitive);
|
|| gatewayUrl.contains(QStringLiteral("::1"), Qt::CaseInsensitive)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#ifdef AMNEZIA_LAN_PLAINTEXT_GATEWAY
|
||||||
|
const QUrl u(gatewayUrl);
|
||||||
|
return NetworkUtilities::hostIsPrivateLanAddress(u.host());
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||||
|
|||||||
@@ -94,7 +94,13 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
|
|||||||
{
|
{
|
||||||
const QUrl gatewayUrl(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl);
|
const QUrl gatewayUrl(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl);
|
||||||
const QString host = gatewayUrl.host().toLower();
|
const QString host = gatewayUrl.host().toLower();
|
||||||
if (host == QLatin1String("localhost") || host == QLatin1String("127.0.0.1") || host == QLatin1String("::1")) {
|
bool usePlaintext = (host == QLatin1String("localhost") || host == QLatin1String("127.0.0.1") || host == QLatin1String("::1"));
|
||||||
|
#ifdef AMNEZIA_LAN_PLAINTEXT_GATEWAY
|
||||||
|
if (!usePlaintext) {
|
||||||
|
usePlaintext = NetworkUtilities::hostIsPrivateLanAddress(host);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (usePlaintext) {
|
||||||
encRequestData.isPlaintextLocalGateway = true;
|
encRequestData.isPlaintextLocalGateway = true;
|
||||||
encRequestData.requestBody = QJsonDocument(apiPayload).toJson();
|
encRequestData.requestBody = QJsonDocument(apiPayload).toJson();
|
||||||
return encRequestData;
|
return encRequestData;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
#include "core/utils/errorCodes.h"
|
#include "core/utils/errorCodes.h"
|
||||||
#include "core/utils/routeModes.h"
|
#include "core/utils/routeModes.h"
|
||||||
@@ -260,6 +261,14 @@ QString SecureAppSettingsRepository::getGatewayEndpoint(bool isTestPurchase) con
|
|||||||
|| base.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)) {
|
|| base.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)) {
|
||||||
return m_gatewayEndpoint;
|
return m_gatewayEndpoint;
|
||||||
}
|
}
|
||||||
|
#ifdef AMNEZIA_LAN_PLAINTEXT_GATEWAY
|
||||||
|
{
|
||||||
|
const QUrl gatewayUrl(base);
|
||||||
|
if (NetworkUtilities::hostIsPrivateLanAddress(gatewayUrl.host())) {
|
||||||
|
return m_gatewayEndpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return QString(DEV_AGW_ENDPOINT);
|
return QString(DEV_AGW_ENDPOINT);
|
||||||
}
|
}
|
||||||
return m_gatewayEndpoint;
|
return m_gatewayEndpoint;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
#include <net/if.h>
|
#include <net/if.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <QAbstractSocket>
|
||||||
#include <QHostAddress>
|
#include <QHostAddress>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
|
|
||||||
@@ -491,3 +492,25 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
|||||||
return { gateway, QNetworkInterface::interfaceFromIndex(index) };
|
return { gateway, QNetworkInterface::interfaceFromIndex(index) };
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NetworkUtilities::hostIsPrivateLanAddress(const QString &host)
|
||||||
|
{
|
||||||
|
if (host.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QHostAddress addr(host);
|
||||||
|
if (addr.isNull() || addr.isLoopback()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (addr.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||||
|
return addr.isInSubnet(QHostAddress(QStringLiteral("10.0.0.0")), 8)
|
||||||
|
|| addr.isInSubnet(QHostAddress(QStringLiteral("172.16.0.0")), 12)
|
||||||
|
|| addr.isInSubnet(QHostAddress(QStringLiteral("192.168.0.0")), 16)
|
||||||
|
|| addr.isInSubnet(QHostAddress(QStringLiteral("169.254.0.0")), 16);
|
||||||
|
}
|
||||||
|
if (addr.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||||
|
return addr.isInSubnet(QHostAddress(QStringLiteral("fe80::")), 10)
|
||||||
|
|| addr.isInSubnet(QHostAddress(QStringLiteral("fc00::")), 7);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public:
|
|||||||
static QString netMaskFromIpWithSubnet(const QString ip);
|
static QString netMaskFromIpWithSubnet(const QString ip);
|
||||||
static QString ipAddressFromIpWithSubnet(const QString ip);
|
static QString ipAddressFromIpWithSubnet(const QString ip);
|
||||||
static QStringList summarizeRoutes(const QStringList &ips, const QString cidr);
|
static QStringList summarizeRoutes(const QStringList &ips, const QString cidr);
|
||||||
|
|
||||||
|
/// True for RFC1918 / IPv4 link-local / IPv6 ULA or IPv6 link-local (dev-only LAN gateway with tools/local_gateway).
|
||||||
|
static bool hostIsPrivateLanAddress(const QString &host);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // NETWORKUTILITIES_H
|
#endif // NETWORKUTILITIES_H
|
||||||
|
|||||||
77
tools/local_gateway/LAN_GATEWAY.md
Normal file
77
tools/local_gateway/LAN_GATEWAY.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Local gateway: LAN / Wi‑Fi (macOS host ↔ iOS client)
|
||||||
|
|
||||||
|
This document is the **implementation checklist** for `tools/local_gateway` plus the **AmneziaVPN client** dev flags used with a **plaintext** mock over **private LAN** addresses (not only `127.0.0.1`).
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. Run **`tools/local_gateway`** on a Mac; reach it from an iPhone on the same Wi‑Fi via **`http://<mac-lan-ip>:<port>/`**.
|
||||||
|
2. **`POST /v1/updater_endpoint`** must return a **`url`** the phone can reach (not `http://127.0.0.1:8080`, which points at the phone itself).
|
||||||
|
3. **Verbose logs** on the server for debugging.
|
||||||
|
4. **Client:** plaintext JSON to the mock only for **loopback** by default; **optional** plaintext to **RFC1918 / ULA / link-local** hosts when **`AMNEZIA_LAN_PLAINTEXT_GATEWAY=ON`** (requires **`AMNEZIA_QR_PAIRING_ALLOW=ON`**).
|
||||||
|
|
||||||
|
> **Security:** `AMNEZIA_LAN_PLAINTEXT_GATEWAY` disables transport encryption to any configured gateway whose host parses as a private LAN address. Use **only** in internal dev builds with `tools/local_gateway`, never for production endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase A — Go server (`main.go`)
|
||||||
|
|
||||||
|
| Item | Status |
|
||||||
|
|------|--------|
|
||||||
|
| **`-listen` / `LOCAL_GATEWAY_LISTEN`** | Default `0.0.0.0:8080`; append `:8080` if port omitted. Still **`tcp4`** only (macOS LAN / curl oddities). |
|
||||||
|
| **`-public-base` / `LOCAL_GATEWAY_PUBLIC_BASE`** | Base URL **without** trailing slash; fed into **`POST /v1/updater_endpoint`** as `{"url":…}`. |
|
||||||
|
| **`-auto-public`** (default true) | If `public-base` empty, pick first **non-loopback IPv4** (prefers **private** / link-local over public). |
|
||||||
|
| **`-pairing-ttl` / `-long-poll` / `-rate-limit-excess-after`** | Replace hardcoded **120s** / **120s** / **0** for pairing and Free mock. |
|
||||||
|
| **Startup banner** | Logs listen address, chosen **`publicUpdaterBaseURL`**, and **every** `http://<ipv4>:port/` candidate per interface. |
|
||||||
|
| **Request logging** | `REQ start` (remote, method, path, query, UA, `X-Client-Request-ID`, content type/length) + `REQ end` (status, duration). |
|
||||||
|
| **Pairing logs** | Extra fields on register/complete (`installation_uuid` short, app/os, `config_len`, protocol count). |
|
||||||
|
|
||||||
|
### Example: Mac + iPhone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tools/local_gateway
|
||||||
|
# Explicit base (safest if you have several NICs):
|
||||||
|
LOCAL_GATEWAY_PUBLIC_BASE='http://192.168.1.10:8080' go run -buildvcs=false .
|
||||||
|
|
||||||
|
# Or rely on auto-public (first suitable IPv4 + listen port):
|
||||||
|
go run -buildvcs=false .
|
||||||
|
```
|
||||||
|
|
||||||
|
Firewall: allow incoming TCP on the chosen port (e.g. **8080**) for **local subnet**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Client (CMake + C++)
|
||||||
|
|
||||||
|
| CMake | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| **`AMNEZIA_QR_PAIRING_ALLOW=ON`** | Enables QR pairing + **loopback** plaintext to `localhost` / `127.0.0.1` / `::1` (`GatewayController`). |
|
||||||
|
| **`AMNEZIA_LAN_PLAINTEXT_GATEWAY=ON`** | **Also** sends plaintext JSON when gateway host is **private LAN** per `NetworkUtilities::hostIsPrivateLanAddress` (IPv4: `10/8`, `172.16/12`, `192.168/16`, `169.254/16`; IPv6: `fe80::/10`, `fc00::/7`). **Requires** `AMNEZIA_QR_PAIRING_ALLOW=ON` or CMake **fatal error**. |
|
||||||
|
|
||||||
|
| Code | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `NetworkUtilities::hostIsPrivateLanAddress` | Shared predicate for gateway + pairing + sandbox endpoint retention. |
|
||||||
|
| `GatewayController::prepareRequest` | Plaintext path for LAN when `AMNEZIA_LAN_PLAINTEXT_GATEWAY` is defined. |
|
||||||
|
| `PairingController::isLocalGatewayHost` | Treats LAN like mock for **120s** long-poll alignment with `tools/local_gateway`. |
|
||||||
|
| `SecureAppSettingsRepository::getGatewayEndpoint(true)` | Keeps user’s LAN mock URL under **test purchase** (same idea as loopback). |
|
||||||
|
|
||||||
|
Configure iOS dev build with both options, set gateway in app to **`http://<mac-ip>:8080/`** (trailing slash as today).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Smoke test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tools/local_gateway
|
||||||
|
go run -buildvcs=false . &
|
||||||
|
sleep 1
|
||||||
|
bash verify.sh 'http://127.0.0.1:8080'
|
||||||
|
# or against LAN IP from another shell on same machine:
|
||||||
|
bash verify.sh 'http://192.168.1.10:8080'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `tools/local_gateway/README.md` — quick start and endpoint table.
|
||||||
|
- `docs/local-gateway-mock.md` — client wiring and checklist.
|
||||||
@@ -13,16 +13,32 @@
|
|||||||
```bash
|
```bash
|
||||||
cd tools/local_gateway
|
cd tools/local_gateway
|
||||||
go mod download
|
go mod download
|
||||||
go run .
|
go run -buildvcs=false .
|
||||||
```
|
```
|
||||||
|
|
||||||
Сервер слушает **`0.0.0.0:8080`** (все IPv4‑интерфейсы): с этого Mac — `http://127.0.0.1:8080/`, с телефона в той же LAN — `http://<IP-это-машины>:8080/`.
|
По умолчанию слушает **`0.0.0.0:8080`** (**`tcp4`** только — см. ниже). С Mac: `http://127.0.0.1:8080/`; с телефона в той же Wi‑Fi: `http://<LAN-IP-Mac>:8080/`.
|
||||||
|
|
||||||
Сервер поднимается через **`net.Listen("tcp4", "0.0.0.0:8080")`**, чтобы на macOS не ловить пустой ответ при `curl`/браузере на **LAN‑IP** (частая нестыковка IPv4/IPv6 у `ListenAndServe(":8080", …)`).
|
**Флаги и переменные окружения** (полный чеклист: **[LAN_GATEWAY.md](./LAN_GATEWAY.md)**):
|
||||||
|
|
||||||
После `git pull` обязательно **остановите старый процесс** на 8080 (`Ctrl+C` в терминале или `kill <PID>`), иначе будет крутиться бинарник без правок.
|
| Флаг / env | Назначение |
|
||||||
|
|------------|------------|
|
||||||
|
| `-listen` / `LOCAL_GATEWAY_LISTEN` | Адрес привязки, например `0.0.0.0:8080` (порт можно опустить — подставится `:8080`). |
|
||||||
|
| `-public-base` / `LOCAL_GATEWAY_PUBLIC_BASE` | Базовый URL **без** хвостового `/` для тела **`POST /v1/updater_endpoint`** (`url`). Нужен для **iOS по LAN** (иначе `127.0.0.1` указывает на сам телефон). |
|
||||||
|
| `-auto-public` (по умолчанию `true`) | Если `public-base` пуст, взять первый подходящий **не loopback** IPv4 и собрать `http://IP:порт`. |
|
||||||
|
| `-pairing-ttl`, `-long-poll` | TTL сессии QR и long-poll (по умолчанию **120s**, как раньше). |
|
||||||
|
| `-rate-limit-excess-after` | Порог для мока Amnezia Free / CAPTCHA (по умолчанию **0**). |
|
||||||
|
|
||||||
В логах при старте: `plaintext mock on tcp4 0.0.0.0:8080 — see ... README.md for paths`. Каждый запрос дополнительно пишется как `REQ <METHOD> <path>`.
|
Пример для iPhone + Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LOCAL_GATEWAY_PUBLIC_BASE='http://192.168.1.10:8080' go run -buildvcs=false .
|
||||||
|
```
|
||||||
|
|
||||||
|
`net.Listen("tcp4", …)` оставлен, чтобы на macOS не ловить пустой ответ при `curl`/браузере на **LAN‑IPv4** (частая нестыковка IPv4/IPv6 у `ListenAndServe(":8080", …)`).
|
||||||
|
|
||||||
|
После `git pull` обязательно **остановите старый процесс** на порту (`Ctrl+C` или `kill <PID>`).
|
||||||
|
|
||||||
|
**Логи:** при старте печатается баннер с **`updater_endpoint` URL** и списком **`http://<ipv4>:порт/`** по интерфейсам. Каждый запрос: строки **`REQ start`** (remote addr, path, UA, `X-Client-Request-ID`, размер тела) и **`REQ end`** (HTTP status, длительность).
|
||||||
|
|
||||||
Проверка без клиента (mock должен быть запущен):
|
Проверка без клиента (mock должен быть запущен):
|
||||||
|
|
||||||
@@ -45,7 +61,7 @@ bash verify.sh http://127.0.0.1:8080
|
|||||||
| `POST` | `/v1/config` | Amnezia Free: CAPTCHA/лимит; иначе короткий мок‑ответ (полноценный premium `vpn://` здесь не строится). |
|
| `POST` | `/v1/config` | Amnezia Free: CAPTCHA/лимит; иначе короткий мок‑ответ (полноценный premium `vpn://` здесь не строится). |
|
||||||
| `POST` | `/v1/news` | Лента новостей (`NewsController`), пустой `news`. |
|
| `POST` | `/v1/news` | Лента новостей (`NewsController`), пустой `news`. |
|
||||||
| `POST` | `/v1/renewal_link` | Ссылка продления (`renewal_url`). |
|
| `POST` | `/v1/renewal_link` | Ссылка продления (`renewal_url`). |
|
||||||
| `POST` | `/v1/updater_endpoint` | `{"url":"http://127.0.0.1:8080"}` → затем GET `/VERSION` на этом хосте. |
|
| `POST` | `/v1/updater_endpoint` | `{"url":"…"}` — база из **`-public-base` / `LOCAL_GATEWAY_PUBLIC_BASE`** или **`-auto-public`**; затем клиент делает GET `/VERSION` на этом хосте. |
|
||||||
| `POST` | `/v1/revoke_config` | Успех, тело не разбирается при `NoError`. |
|
| `POST` | `/v1/revoke_config` | Успех, тело не разбирается при `NoError`. |
|
||||||
| `POST` | `/v1/revoke_native_config` | То же. |
|
| `POST` | `/v1/revoke_native_config` | То же. |
|
||||||
| `POST` | `/api/v1/generate_qr` | Pairing: long-poll (**120s** mock). |
|
| `POST` | `/api/v1/generate_qr` | Pairing: long-poll (**120s** mock). |
|
||||||
@@ -57,8 +73,9 @@ bash verify.sh http://127.0.0.1:8080
|
|||||||
|
|
||||||
## Связка с клиентом AmneziaVPN
|
## Связка с клиентом AmneziaVPN
|
||||||
|
|
||||||
1. Соберите клиент с определением **`AMNEZIA_LOCAL_GATEWAY`** (см. `client/CMakeLists.txt`, `target_compile_definitions`) — тогда для **`127.0.0.1`** и **`localhost`** запросы к gateway уходят **plaintext JSON** без RSA/AES (см. `GatewayController`, `SecureAppSettingsRepository`).
|
1. Включите **`AMNEZIA_QR_PAIRING_ALLOW=ON`** в CMake — тогда для **`127.0.0.1`**, **`localhost`**, **`::1`** запросы к gateway идут **plaintext JSON** без RSA/AES (`GatewayController`).
|
||||||
2. В настройках приложения endpoint gateway: **`http://127.0.0.1:8080/`** (дефолт при `AMNEZIA_LOCAL_GATEWAY` в коде). Допустим и `http://localhost:8080/` — тоже plaintext.
|
2. Для **iOS / другого хоста по LAN-IP** (`192.168.x.x` и т.п.) дополнительно включите **`AMNEZIA_LAN_PLAINTEXT_GATEWAY=ON`** (требует `AMNEZIA_QR_PAIRING_ALLOW`). Иначе клиент шифрует тело как к прод gateway, mock его не поймёт. Подробности: **[LAN_GATEWAY.md](./LAN_GATEWAY.md)**.
|
||||||
|
3. В настройках приложения укажите endpoint: **`http://127.0.0.1:8080/`** или **`http://<LAN-IP>:8080/`** (с завершающим `/`, как принято в репозитории).
|
||||||
|
|
||||||
Пошаговый план (включая следующие этапы вроде `/v1/account_info`): **`docs/local-gateway-mock.md`**.
|
Пошаговый план (включая следующие этапы вроде `/v1/account_info`): **`docs/local-gateway-mock.md`**.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Plaintext mock for AmneziaVPN client (CMake AMNEZIA_LOCAL_GATEWAY=ON + localhost DEV_AGW_ENDPOINT).
|
// Plaintext mock for AmneziaVPN client (CMake AMNEZIA_QR_PAIRING_ALLOW; optional AMNEZIA_LAN_PLAINTEXT_GATEWAY for RFC1918 hosts).
|
||||||
// No RSA/AES — POST JSON is the same object the client sends inside api_payload when encrypted.
|
// No RSA/AES — POST JSON is the same object the client sends inside api_payload when encrypted.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -6,10 +6,15 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,19 +28,17 @@ func shortID(id string) string {
|
|||||||
return id[:10] + "…"
|
return id[:10] + "…"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set to 5 to mimic "more than 5 requests per 24h". Set to 0 so the first amnezia-free request returns CAPTCHA (faster UI test).
|
|
||||||
const rateLimitExcessAfter = 0
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
requests = map[string][]time.Time{} // installation_uuid -> timestamps (sliding window simplified: count in session)
|
requests = map[string][]time.Time{} // installation_uuid -> timestamps (sliding window simplified: count in session)
|
||||||
sessions = map[string]*pairingSession{}
|
sessions = map[string]*pairingSession{}
|
||||||
)
|
|
||||||
|
|
||||||
// Local dev only: gateway team agreed 120s for mock vs 30s production (task_1 docs).
|
// Configured from flags / env in main().
|
||||||
const (
|
pairingSessionTTL = 120 * time.Second
|
||||||
pairingTTL = 120 * time.Second
|
longPollWaitLimit = 120 * time.Second
|
||||||
longPollWaitLimit = 120 * time.Second
|
rateLimitExcessAfter = 0 // Set to 5 to mimic "more than 5 requests per 24h". 0 = first amnezia-free request may return CAPTCHA.
|
||||||
|
// No trailing slash; used by POST /v1/updater_endpoint so remote clients (e.g. iOS) poll the Mac, not 127.0.0.1 on-device.
|
||||||
|
publicUpdaterBaseURL string
|
||||||
)
|
)
|
||||||
|
|
||||||
type generateQRRequest struct {
|
type generateQRRequest struct {
|
||||||
@@ -85,11 +88,40 @@ func drainBody(r *http.Request) {
|
|||||||
_ = r.Body.Close()
|
_ = r.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// logReq logs every request (step 5 in docs/local-gateway-mock.md).
|
// statusResponseWriter captures HTTP status for access-style logging.
|
||||||
|
type statusResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
written bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *statusResponseWriter) WriteHeader(code int) {
|
||||||
|
if !w.written {
|
||||||
|
w.status = code
|
||||||
|
w.written = true
|
||||||
|
}
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *statusResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !w.written {
|
||||||
|
w.status = http.StatusOK
|
||||||
|
w.written = true
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// logReq logs every request with remote addr, UA, and final status (docs/local-gateway-mock.md).
|
||||||
func logReq(next http.HandlerFunc) http.HandlerFunc {
|
func logReq(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("REQ %s %s", r.Method, r.URL.Path)
|
srw := &statusResponseWriter{ResponseWriter: w, status: http.StatusOK}
|
||||||
next(w, r)
|
start := time.Now()
|
||||||
|
log.Printf("REQ start remote=%s method=%s path=%s query=%s ua=%q x_client_request_id=%q content_type=%q content_length=%d",
|
||||||
|
r.RemoteAddr, r.Method, r.URL.Path, r.URL.RawQuery, r.Header.Get("User-Agent"), r.Header.Get("X-Client-Request-ID"),
|
||||||
|
r.Header.Get("Content-Type"), r.ContentLength)
|
||||||
|
next(srw, r)
|
||||||
|
log.Printf("REQ end remote=%s method=%s path=%s status=%d dur=%s",
|
||||||
|
r.RemoteAddr, r.Method, r.URL.Path, srw.status, time.Since(start).Round(time.Millisecond))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +185,7 @@ func handleGenerateQR(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
session := &pairingSession{
|
session := &pairingSession{
|
||||||
QRUUID: req.QRUUID,
|
QRUUID: req.QRUUID,
|
||||||
ExpiresAt: time.Now().Add(pairingTTL),
|
ExpiresAt: time.Now().Add(pairingSessionTTL),
|
||||||
Done: make(chan struct{}),
|
Done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +194,8 @@ func handleGenerateQR(w http.ResponseWriter, r *http.Request) {
|
|||||||
sessions[req.QRUUID] = session
|
sessions[req.QRUUID] = session
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
log.Printf("pairing REGISTERED uuid=%s ttl=%s", shortID(req.QRUUID), pairingTTL)
|
log.Printf("pairing REGISTERED uuid=%s install=%s ttl=%s app=%s os=%s",
|
||||||
|
shortID(req.QRUUID), shortID(req.InstallationUUID), pairingSessionTTL, req.AppVersion, req.OSVersion)
|
||||||
|
|
||||||
timer := time.NewTimer(longPollWaitLimit)
|
timer := time.NewTimer(longPollWaitLimit)
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
@@ -247,7 +280,8 @@ func handleScanQR(w http.ResponseWriter, r *http.Request) {
|
|||||||
close(session.Done)
|
close(session.Done)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
log.Printf("pairing COMPLETED uuid=%s config_len=%d", shortID(req.QRUUID), len(req.Config))
|
log.Printf("pairing COMPLETED uuid=%s phone_install=%s config_len=%d proto_count=%d",
|
||||||
|
shortID(req.QRUUID), shortID(req.InstallationUUID), len(req.Config), len(req.SupportedProto))
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"message": "OK"})
|
writeJSON(w, http.StatusOK, map[string]string{"message": "OK"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +491,8 @@ func handleUpdaterEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
drainBody(r)
|
drainBody(r)
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"url": "http://127.0.0.1:8080"})
|
log.Printf("updater_endpoint response url=%q", publicUpdaterBaseURL)
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"url": publicUpdaterBaseURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /v1/revoke_config, /v1/revoke_native_config — success body ignored if error is NoError.
|
// POST /v1/revoke_config, /v1/revoke_native_config — success body ignored if error is NoError.
|
||||||
@@ -498,7 +533,148 @@ func handleGetReleaseDate(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envOrDefault(key, def string) string {
|
||||||
|
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneIPv4(ip net.IP) net.IP {
|
||||||
|
x := make(net.IP, 4)
|
||||||
|
copy(x, ip.To4())
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// pickLANIPv4 returns a stable choice of non-loopback IPv4 for updater_endpoint / banners (prefers private ULA space).
|
||||||
|
func pickLANIPv4() net.IP {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("net.Interfaces: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
type cand struct {
|
||||||
|
ip net.IP
|
||||||
|
private bool
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
var cands []cand
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.Flags&net.FlagUp == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if iface.Flags&net.FlagLoopback != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, a := range addrs {
|
||||||
|
ipNet, ok := a.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip4 := ipNet.IP.To4()
|
||||||
|
if ip4 == nil || ip4.IsLoopback() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
priv := ip4.IsPrivate() || ip4.IsLinkLocalUnicast()
|
||||||
|
cands = append(cands, cand{ip: cloneIPv4(ip4), private: priv, name: iface.Name})
|
||||||
|
log.Printf("iface candidate name=%s ip=%s private_or_linklocal=%v", iface.Name, ip4, priv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cands) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sort.SliceStable(cands, func(i, j int) bool {
|
||||||
|
if cands[i].private != cands[j].private {
|
||||||
|
return cands[i].private
|
||||||
|
}
|
||||||
|
if cands[i].name != cands[j].name {
|
||||||
|
return cands[i].name < cands[j].name
|
||||||
|
}
|
||||||
|
return bytes.Compare(cands[i].ip, cands[j].ip) < 0
|
||||||
|
})
|
||||||
|
chosen := cands[0].ip
|
||||||
|
log.Printf("pickLANIPv4: using %s (iface_hint=%s)", chosen, cands[0].name)
|
||||||
|
return chosen
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePublicBase(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
s = strings.TrimSuffix(s, "/")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func logStartupURLs(listenAddr, portStr string) {
|
||||||
|
log.Printf("=== local_gateway (plaintext mock) ===")
|
||||||
|
log.Printf("listen tcp4: %s", listenAddr)
|
||||||
|
log.Printf("POST /v1/updater_endpoint will return: {\"url\": %q}", publicUpdaterBaseURL)
|
||||||
|
log.Printf("Point AmneziaVPN gateway setting to: %s/", publicUpdaterBaseURL)
|
||||||
|
log.Printf("Try from phone browser: %s/", publicUpdaterBaseURL)
|
||||||
|
log.Printf("Non-loopback IPv4 URLs (same listen port %s):", portStr)
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" (could not enumerate interfaces: %v)", err)
|
||||||
|
} else {
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrs, _ := iface.Addrs()
|
||||||
|
for _, a := range addrs {
|
||||||
|
ipNet, ok := a.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip4 := ipNet.IP.To4(); ip4 != nil && !ip4.IsLoopback() {
|
||||||
|
log.Printf(" http://%s:%s/ (iface %s)", ip4, portStr, iface.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("docs: tools/local_gateway/README.md tools/local_gateway/LAN_GATEWAY.md")
|
||||||
|
log.Printf("========================================")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
listenFlag := flag.String("listen", envOrDefault("LOCAL_GATEWAY_LISTEN", "0.0.0.0:8080"),
|
||||||
|
"TCP listen address (tcp4). Env: LOCAL_GATEWAY_LISTEN")
|
||||||
|
publicFlag := flag.String("public-base", strings.TrimSpace(os.Getenv("LOCAL_GATEWAY_PUBLIC_BASE")),
|
||||||
|
"Base URL without trailing slash for /v1/updater_endpoint (required for iOS-on-LAN). Env: LOCAL_GATEWAY_PUBLIC_BASE")
|
||||||
|
autoPublic := flag.Bool("auto-public", true, "If public-base empty, derive http://<first-lan-ipv4>:port")
|
||||||
|
pairTTL := flag.Duration("pairing-ttl", 120*time.Second, "QR pairing session TTL")
|
||||||
|
longPoll := flag.Duration("long-poll", 120*time.Second, "Long-poll max wait for POST /api/v1/generate_qr")
|
||||||
|
rateN := flag.Int("rate-limit-excess-after", 0, "Amnezia Free: allow N requests per 24h window before rate-limit/CAPTCHA (0=tight)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
listenAddr := strings.TrimSpace(*listenFlag)
|
||||||
|
if _, _, err := net.SplitHostPort(listenAddr); err != nil {
|
||||||
|
listenAddr = net.JoinHostPort(listenAddr, "8080")
|
||||||
|
}
|
||||||
|
_, portStr, err := net.SplitHostPort(listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("listen address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pairingSessionTTL = *pairTTL
|
||||||
|
longPollWaitLimit = *longPoll
|
||||||
|
rateLimitExcessAfter = *rateN
|
||||||
|
|
||||||
|
pub := normalizePublicBase(*publicFlag)
|
||||||
|
if pub == "" && *autoPublic {
|
||||||
|
if ip := pickLANIPv4(); ip != nil {
|
||||||
|
pub = fmt.Sprintf("http://%s:%s", ip.String(), portStr)
|
||||||
|
log.Printf("auto-public: updater + docs base -> %s (override with -public-base or LOCAL_GATEWAY_PUBLIC_BASE)", pub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pub == "" {
|
||||||
|
pub = fmt.Sprintf("http://127.0.0.1:%s", portStr)
|
||||||
|
log.Printf("WARN: public-base not set and auto-public found no LAN IPv4; using %s (broken for remote phones). Set -public-base or LOCAL_GATEWAY_PUBLIC_BASE.", pub)
|
||||||
|
}
|
||||||
|
publicUpdaterBaseURL = pub
|
||||||
|
|
||||||
http.HandleFunc("/", logReq(handleRoot))
|
http.HandleFunc("/", logReq(handleRoot))
|
||||||
http.HandleFunc("/VERSION", logReq(handleGetVersion))
|
http.HandleFunc("/VERSION", logReq(handleGetVersion))
|
||||||
http.HandleFunc("/CHANGELOG", logReq(handleGetChangelog))
|
http.HandleFunc("/CHANGELOG", logReq(handleGetChangelog))
|
||||||
@@ -513,11 +689,13 @@ func main() {
|
|||||||
http.HandleFunc("/v1/revoke_native_config", logReq(handleRevokeNoop))
|
http.HandleFunc("/v1/revoke_native_config", logReq(handleRevokeNoop))
|
||||||
http.HandleFunc("/api/v1/generate_qr", logReq(handleGenerateQR))
|
http.HandleFunc("/api/v1/generate_qr", logReq(handleGenerateQR))
|
||||||
http.HandleFunc("/api/v1/scan_qr", logReq(handleScanQR))
|
http.HandleFunc("/api/v1/scan_qr", logReq(handleScanQR))
|
||||||
const addr = "0.0.0.0:8080"
|
|
||||||
log.Printf("plaintext mock on tcp4 %s — see tools/local_gateway/README.md for paths\n", addr)
|
logStartupURLs(listenAddr, portStr)
|
||||||
ln, err := net.Listen("tcp4", addr)
|
|
||||||
|
ln, err := net.Listen("tcp4", listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
log.Printf("listening tcp4 %s (actual %v)", listenAddr, ln.Addr())
|
||||||
log.Fatal(http.Serve(ln, nil))
|
log.Fatal(http.Serve(ln, nil))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
BASE="${1:-http://127.0.0.1:8080}"
|
BASE="${1:-http://127.0.0.1:8080}"
|
||||||
|
|
||||||
|
echo "== local_gateway verify base: ${BASE} =="
|
||||||
|
|
||||||
echo "== GET / =="
|
echo "== GET / =="
|
||||||
curl -sfS "$BASE/" | head -n 2
|
curl -sfS "$BASE/" | head -n 2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user