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)
|
||||
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})
|
||||
|
||||
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QSysInfo>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -22,10 +24,18 @@ constexpr qsizetype kPairingMaxApiKeyChars = 8192;
|
||||
|
||||
bool isLocalGatewayHost(const QString &gatewayUrl)
|
||||
{
|
||||
return gatewayUrl.contains(QStringLiteral("127.0.0.1"), Qt::CaseInsensitive)
|
||||
|| gatewayUrl.contains(QStringLiteral("localhost"), Qt::CaseInsensitive)
|
||||
|| gatewayUrl.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)
|
||||
|| gatewayUrl.contains(QStringLiteral("::1"), Qt::CaseInsensitive);
|
||||
if (gatewayUrl.contains(QStringLiteral("127.0.0.1"), Qt::CaseInsensitive)
|
||||
|| gatewayUrl.contains(QStringLiteral("localhost"), 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)
|
||||
|
||||
@@ -94,7 +94,13 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
|
||||
{
|
||||
const QUrl gatewayUrl(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl);
|
||||
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.requestBody = QJsonDocument(apiPayload).toJson();
|
||||
return encRequestData;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QUuid>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
@@ -260,6 +261,14 @@ QString SecureAppSettingsRepository::getGatewayEndpoint(bool isTestPurchase) con
|
||||
|| base.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)) {
|
||||
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 m_gatewayEndpoint;
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <net/if.h>
|
||||
#endif
|
||||
|
||||
#include <QAbstractSocket>
|
||||
#include <QHostAddress>
|
||||
#include <QHostInfo>
|
||||
|
||||
@@ -491,3 +492,25 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
return { gateway, QNetworkInterface::interfaceFromIndex(index) };
|
||||
#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 ipAddressFromIpWithSubnet(const QString ip);
|
||||
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
|
||||
|
||||
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
|
||||
cd tools/local_gateway
|
||||
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 должен быть запущен):
|
||||
|
||||
@@ -45,7 +61,7 @@ bash verify.sh http://127.0.0.1:8080
|
||||
| `POST` | `/v1/config` | Amnezia Free: CAPTCHA/лимит; иначе короткий мок‑ответ (полноценный premium `vpn://` здесь не строится). |
|
||||
| `POST` | `/v1/news` | Лента новостей (`NewsController`), пустой `news`. |
|
||||
| `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_native_config` | То же. |
|
||||
| `POST` | `/api/v1/generate_qr` | Pairing: long-poll (**120s** mock). |
|
||||
@@ -57,8 +73,9 @@ bash verify.sh http://127.0.0.1:8080
|
||||
|
||||
## Связка с клиентом AmneziaVPN
|
||||
|
||||
1. Соберите клиент с определением **`AMNEZIA_LOCAL_GATEWAY`** (см. `client/CMakeLists.txt`, `target_compile_definitions`) — тогда для **`127.0.0.1`** и **`localhost`** запросы к gateway уходят **plaintext JSON** без RSA/AES (см. `GatewayController`, `SecureAppSettingsRepository`).
|
||||
2. В настройках приложения endpoint gateway: **`http://127.0.0.1:8080/`** (дефолт при `AMNEZIA_LOCAL_GATEWAY` в коде). Допустим и `http://localhost:8080/` — тоже plaintext.
|
||||
1. Включите **`AMNEZIA_QR_PAIRING_ALLOW=ON`** в CMake — тогда для **`127.0.0.1`**, **`localhost`**, **`::1`** запросы к gateway идут **plaintext JSON** без RSA/AES (`GatewayController`).
|
||||
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`**.
|
||||
|
||||
|
||||
@@ -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.
|
||||
package main
|
||||
|
||||
@@ -6,10 +6,15 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -23,19 +28,17 @@ func shortID(id string) string {
|
||||
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 (
|
||||
mu sync.Mutex
|
||||
requests = map[string][]time.Time{} // installation_uuid -> timestamps (sliding window simplified: count in session)
|
||||
sessions = map[string]*pairingSession{}
|
||||
)
|
||||
|
||||
// Local dev only: gateway team agreed 120s for mock vs 30s production (task_1 docs).
|
||||
const (
|
||||
pairingTTL = 120 * time.Second
|
||||
longPollWaitLimit = 120 * time.Second
|
||||
// Configured from flags / env in main().
|
||||
pairingSessionTTL = 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 {
|
||||
@@ -85,11 +88,40 @@ func drainBody(r *http.Request) {
|
||||
_ = 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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("REQ %s %s", r.Method, r.URL.Path)
|
||||
next(w, r)
|
||||
srw := &statusResponseWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
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{
|
||||
QRUUID: req.QRUUID,
|
||||
ExpiresAt: time.Now().Add(pairingTTL),
|
||||
ExpiresAt: time.Now().Add(pairingSessionTTL),
|
||||
Done: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -162,7 +194,8 @@ func handleGenerateQR(w http.ResponseWriter, r *http.Request) {
|
||||
sessions[req.QRUUID] = session
|
||||
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)
|
||||
defer timer.Stop()
|
||||
@@ -247,7 +280,8 @@ func handleScanQR(w http.ResponseWriter, r *http.Request) {
|
||||
close(session.Done)
|
||||
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"})
|
||||
}
|
||||
|
||||
@@ -457,7 +491,8 @@ func handleUpdaterEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
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.
|
||||
@@ -498,7 +533,148 @@ func handleGetReleaseDate(w http.ResponseWriter, r *http.Request) {
|
||||
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() {
|
||||
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("/VERSION", logReq(handleGetVersion))
|
||||
http.HandleFunc("/CHANGELOG", logReq(handleGetChangelog))
|
||||
@@ -513,11 +689,13 @@ func main() {
|
||||
http.HandleFunc("/v1/revoke_native_config", logReq(handleRevokeNoop))
|
||||
http.HandleFunc("/api/v1/generate_qr", logReq(handleGenerateQR))
|
||||
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)
|
||||
ln, err := net.Listen("tcp4", addr)
|
||||
|
||||
logStartupURLs(listenAddr, portStr)
|
||||
|
||||
ln, err := net.Listen("tcp4", listenAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("listening tcp4 %s (actual %v)", listenAddr, ln.Addr())
|
||||
log.Fatal(http.Serve(ln, nil))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
set -euo pipefail
|
||||
BASE="${1:-http://127.0.0.1:8080}"
|
||||
|
||||
echo "== local_gateway verify base: ${BASE} =="
|
||||
|
||||
echo "== GET / =="
|
||||
curl -sfS "$BASE/" | head -n 2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user