feat: improve macos client proxy setup
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
PORT=3456
|
PORT=3456
|
||||||
APP_MODE=gateway
|
APP_MODE=gateway
|
||||||
CLIENT_UI_PORT=3456
|
CLIENT_UI_PORT=3456
|
||||||
CLIENT_PROXY_PORT=8080
|
CLIENT_PROXY_PORT_START=8080
|
||||||
|
CLIENT_PROXY_PORT_END=8090
|
||||||
BASE_IMAGE=debian:bookworm-slim
|
BASE_IMAGE=debian:bookworm-slim
|
||||||
SINGBOX_VERSION=1.12.13
|
SINGBOX_VERSION=1.12.13
|
||||||
INSTALL_RUNTIME_DEPS=true
|
INSTALL_RUNTIME_DEPS=true
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -13,9 +13,17 @@ curl -fsSL https://git.dokops.ru/dokril/vpn-proxy/raw/branch/master/scripts/inst
|
|||||||
После запуска:
|
После запуска:
|
||||||
|
|
||||||
- UI: `http://127.0.0.1:3456`
|
- UI: `http://127.0.0.1:3456`
|
||||||
- HTTP/SOCKS proxy: `127.0.0.1:8080`
|
- HTTP/SOCKS proxy: `127.0.0.1:8080` по умолчанию; в UI можно выбрать порт из Docker-диапазона `8080–8090`
|
||||||
|
|
||||||
В Mac UI есть **Домашний режим**. Когда он включён, приложения по-прежнему используют `127.0.0.1:8080`, но весь proxy-трафик идёт напрямую без VPN.
|
Установщик интерактивно спросит proxy-порт. Для неинтерактивного запуска можно задать его заранее; тогда вопрос не появится:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.dokops.ru/dokril/vpn-proxy/raw/branch/master/scripts/install-macos-client.sh | VPN_PROXY_CLIENT_PORT=18080 bash
|
||||||
|
```
|
||||||
|
|
||||||
|
После запуска скрипт проверяет, что UI реально ответил на `/api/state`. Если контейнер сразу упал или порт занят, он покажет `docker compose ps` и последние логи вместо ложного сообщения о готовности.
|
||||||
|
|
||||||
|
В Mac UI есть **Домашний режим**. Когда он включён, приложения по-прежнему используют выбранный локальный proxy-порт, но весь proxy-трафик идёт напрямую без VPN.
|
||||||
|
|
||||||
Ручной запуск из checkout:
|
Ручной запуск из checkout:
|
||||||
|
|
||||||
@@ -343,7 +351,8 @@ UI доступен на `http://<gateway-ip>:3456`.
|
|||||||
| ------------------- | -------------------- | -------------------------------------- |
|
| ------------------- | -------------------- | -------------------------------------- |
|
||||||
| `APP_MODE` | `gateway` | `gateway` или `client`; compose клиента задаёт `client` автоматически |
|
| `APP_MODE` | `gateway` | `gateway` или `client`; compose клиента задаёт `client` автоматически |
|
||||||
| `CLIENT_UI_PORT` | `3456` | Host-порт UI для `docker-compose.client.yml` |
|
| `CLIENT_UI_PORT` | `3456` | Host-порт UI для `docker-compose.client.yml` |
|
||||||
| `CLIENT_PROXY_PORT` | `8080` | Host-порт proxy для `docker-compose.client.yml` |
|
| `CLIENT_PROXY_PORT_START` | `8080` | Первый host/container proxy-порт для `docker-compose.client.yml` |
|
||||||
|
| `CLIENT_PROXY_PORT_END` | `8090` | Последний host/container proxy-порт для `docker-compose.client.yml` |
|
||||||
| `PORT` | `3456` | Порт веб-интерфейса |
|
| `PORT` | `3456` | Порт веб-интерфейса |
|
||||||
| `BASE_IMAGE` | `debian:bookworm-slim` | Базовый Docker image для сборки; можно заменить на mirror |
|
| `BASE_IMAGE` | `debian:bookworm-slim` | Базовый Docker image для сборки; можно заменить на mirror |
|
||||||
| `SINGBOX_VERSION` | `1.12.13` | Версия sing-box для Docker build |
|
| `SINGBOX_VERSION` | `1.12.13` | Версия sing-box для Docker build |
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
APP_MODE: client
|
APP_MODE: client
|
||||||
PORT: ${PORT:-3456}
|
PORT: ${PORT:-3456}
|
||||||
PROXY_PORT: ${PROXY_PORT:-8080}
|
PROXY_PORT: ${CLIENT_PROXY_PORT_START:-8080}
|
||||||
|
CLIENT_PROXY_PORT_START: ${CLIENT_PROXY_PORT_START:-8080}
|
||||||
|
CLIENT_PROXY_PORT_END: ${CLIENT_PROXY_PORT_END:-8090}
|
||||||
PROXY_BIND_IP: 0.0.0.0
|
PROXY_BIND_IP: 0.0.0.0
|
||||||
DATA_DIR: /var/lib/vpn-proxy
|
DATA_DIR: /var/lib/vpn-proxy
|
||||||
SING_BOX_CONFIG: /etc/sing-box/config.json
|
SING_BOX_CONFIG: /etc/sing-box/config.json
|
||||||
@@ -19,7 +21,7 @@ services:
|
|||||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${CLIENT_UI_PORT:-3456}:${PORT:-3456}"
|
- "127.0.0.1:${CLIENT_UI_PORT:-3456}:${PORT:-3456}"
|
||||||
- "127.0.0.1:${CLIENT_PROXY_PORT:-8080}:${PROXY_PORT:-8080}"
|
- "127.0.0.1:${CLIENT_PROXY_PORT_START:-8080}-${CLIENT_PROXY_PORT_END:-8090}:${CLIENT_PROXY_PORT_START:-8080}-${CLIENT_PROXY_PORT_END:-8090}"
|
||||||
volumes:
|
volumes:
|
||||||
- vpn-proxy-client-data:/var/lib/vpn-proxy
|
- vpn-proxy-client-data:/var/lib/vpn-proxy
|
||||||
- sing-box-client-cache:/var/lib/sing-box
|
- sing-box-client-cache:/var/lib/sing-box
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ INSTALL_DIR="${VPN_PROXY_INSTALL_DIR:-$HOME/.vpn-proxy-client}"
|
|||||||
REPO_URL="${VPN_PROXY_REPO_URL:-https://git.dokops.ru/dokril/vpn-proxy.git}"
|
REPO_URL="${VPN_PROXY_REPO_URL:-https://git.dokops.ru/dokril/vpn-proxy.git}"
|
||||||
BRANCH="${VPN_PROXY_BRANCH:-master}"
|
BRANCH="${VPN_PROXY_BRANCH:-master}"
|
||||||
COMPOSE_FILE="docker-compose.client.yml"
|
COMPOSE_FILE="docker-compose.client.yml"
|
||||||
|
DEFAULT_PROXY_PORT="8080"
|
||||||
|
REQUESTED_PROXY_PORT="${VPN_PROXY_CLIENT_PORT:-}"
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
printf '[vpn-proxy-client] %s\n' "$*"
|
printf '[vpn-proxy-client] %s\n' "$*"
|
||||||
@@ -19,12 +21,98 @@ need() {
|
|||||||
command -v "$1" >/dev/null 2>&1 || die "$1 is required"
|
command -v "$1" >/dev/null 2>&1 || die "$1 is required"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_valid_port() {
|
||||||
|
case "$1" in
|
||||||
|
''|*[!0-9]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
[ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
ask_proxy_port() {
|
||||||
|
local value=""
|
||||||
|
if [ -n "$REQUESTED_PROXY_PORT" ]; then
|
||||||
|
if ! is_valid_port "$REQUESTED_PROXY_PORT"; then
|
||||||
|
die "VPN_PROXY_CLIENT_PORT must be a port from 1024 to 65535"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$REQUESTED_PROXY_PORT"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -r /dev/tty ]; then
|
||||||
|
while true; do
|
||||||
|
printf 'Proxy port for local apps [%s]: ' "$DEFAULT_PROXY_PORT" >/dev/tty
|
||||||
|
IFS= read -r value </dev/tty || value=""
|
||||||
|
value="${value:-$DEFAULT_PROXY_PORT}"
|
||||||
|
if is_valid_port "$value"; then
|
||||||
|
printf '%s\n' "$value"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'Enter a port from 1024 to 65535.\n' >/dev/tty
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_valid_port "$DEFAULT_PROXY_PORT"; then
|
||||||
|
die "VPN_PROXY_CLIENT_PORT must be a port from 1024 to 65535"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$DEFAULT_PROXY_PORT"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_client_ui() {
|
||||||
|
local ui_port="${UI_PORT:-3456}"
|
||||||
|
local ui_url="http://127.0.0.1:${ui_port}/api/state"
|
||||||
|
local attempt
|
||||||
|
|
||||||
|
for attempt in $(seq 1 30); do
|
||||||
|
if curl -fsS "$ui_url" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n[vpn-proxy-client] client did not become ready at %s\n' "$ui_url" >&2
|
||||||
|
printf '[vpn-proxy-client] docker compose status:\n' >&2
|
||||||
|
docker compose -f "$COMPOSE_FILE" ps >&2 || true
|
||||||
|
printf '\n[vpn-proxy-client] recent service logs:\n' >&2
|
||||||
|
docker compose -f "$COMPOSE_FILE" logs --tail=120 vpn-proxy-client >&2 || true
|
||||||
|
die "client UI is not ready; see Docker status and logs above"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
|
||||||
|
if [ -f .env ] && grep -q "^${key}=" .env; then
|
||||||
|
awk -v key="$key" -v value="$value" '
|
||||||
|
BEGIN { prefix = key "=" }
|
||||||
|
index($0, prefix) == 1 { print key "=" value; next }
|
||||||
|
{ print }
|
||||||
|
' .env > "$tmp"
|
||||||
|
else
|
||||||
|
[ -f .env ] && cat .env > "$tmp"
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$tmp" .env
|
||||||
|
}
|
||||||
|
|
||||||
|
get_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
[ -f .env ] || return 0
|
||||||
|
awk -v key="$key" '
|
||||||
|
BEGIN { prefix = key "=" }
|
||||||
|
index($0, prefix) == 1 { print substr($0, length(prefix) + 1); exit }
|
||||||
|
' .env
|
||||||
|
}
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
die "this installer is intended for macOS"
|
die "this installer is intended for macOS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
need git
|
need git
|
||||||
need docker
|
need docker
|
||||||
|
need curl
|
||||||
|
|
||||||
docker compose version >/dev/null 2>&1 || die "Docker Compose plugin is required"
|
docker compose version >/dev/null 2>&1 || die "Docker Compose plugin is required"
|
||||||
docker info >/dev/null 2>&1 || die "Docker Desktop is not running"
|
docker info >/dev/null 2>&1 || die "Docker Desktop is not running"
|
||||||
@@ -46,18 +134,35 @@ if [[ ! -f .env && -f .env.example ]]; then
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
PROXY_PORT="$(ask_proxy_port)"
|
||||||
|
PROXY_PORT_END="$((PROXY_PORT + 10))"
|
||||||
|
if [ "$PROXY_PORT_END" -gt 65535 ]; then
|
||||||
|
PROXY_PORT_END=65535
|
||||||
|
fi
|
||||||
|
UI_PORT="${CLIENT_UI_PORT:-$(get_env_value CLIENT_UI_PORT)}"
|
||||||
|
UI_PORT="${UI_PORT:-3456}"
|
||||||
|
|
||||||
|
set_env_value APP_MODE client
|
||||||
|
set_env_value CLIENT_PROXY_PORT_START "$PROXY_PORT"
|
||||||
|
set_env_value CLIENT_PROXY_PORT_END "$PROXY_PORT_END"
|
||||||
|
set_env_value PROXY_PORT "$PROXY_PORT"
|
||||||
|
|
||||||
|
log "proxy port: 127.0.0.1:${PROXY_PORT} (reserved range ${PROXY_PORT}-${PROXY_PORT_END})"
|
||||||
|
|
||||||
log "building and starting Docker client"
|
log "building and starting Docker client"
|
||||||
docker compose -f "$COMPOSE_FILE" up -d --build
|
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||||
|
wait_for_client_ui
|
||||||
|
|
||||||
cat <<'EOF'
|
cat <<EOF
|
||||||
|
|
||||||
VPN Proxy Client is running.
|
VPN Proxy Client is running.
|
||||||
|
|
||||||
UI:
|
UI:
|
||||||
http://127.0.0.1:3456
|
http://127.0.0.1:${UI_PORT}
|
||||||
|
|
||||||
Proxy:
|
Proxy:
|
||||||
HTTP/SOCKS5 127.0.0.1:8080
|
HTTP/SOCKS5 127.0.0.1:${PROXY_PORT}
|
||||||
|
UI can switch proxy port within the Docker-published ${PROXY_PORT}-${PROXY_PORT_END} range.
|
||||||
|
|
||||||
Useful commands:
|
Useful commands:
|
||||||
cd ~/.vpn-proxy-client
|
cd ~/.vpn-proxy-client
|
||||||
@@ -66,9 +171,9 @@ Useful commands:
|
|||||||
docker compose -f docker-compose.client.yml down
|
docker compose -f docker-compose.client.yml down
|
||||||
|
|
||||||
Optional macOS system proxy example:
|
Optional macOS system proxy example:
|
||||||
networksetup -setwebproxy Wi-Fi 127.0.0.1 8080
|
networksetup -setwebproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
|
||||||
networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 8080
|
networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
|
||||||
networksetup -setsocksfirewallproxy Wi-Fi 127.0.0.1 8080
|
networksetup -setsocksfirewallproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
|
||||||
|
|
||||||
Disable later:
|
Disable later:
|
||||||
networksetup -setwebproxystate Wi-Fi off
|
networksetup -setwebproxystate Wi-Fi off
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ const DEFAULT_CLIENT_SETTINGS = {
|
|||||||
homeBypassEnabled: false,
|
homeBypassEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeProxyPort(value, fallback = settings.proxyPort) {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
const min = Number.isInteger(settings.clientProxyPortStart)
|
||||||
|
? settings.clientProxyPortStart
|
||||||
|
: settings.proxyPort;
|
||||||
|
const max = Number.isInteger(settings.clientProxyPortEnd)
|
||||||
|
? settings.clientProxyPortEnd
|
||||||
|
: min;
|
||||||
|
const fallbackPort =
|
||||||
|
Number.isInteger(fallback) && fallback >= min && fallback <= max
|
||||||
|
? fallback
|
||||||
|
: min;
|
||||||
|
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
||||||
|
return fallbackPort;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
function readJson(filePath, fallback) {
|
function readJson(filePath, fallback) {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(filePath)) return fallback;
|
if (!fs.existsSync(filePath)) return fallback;
|
||||||
@@ -23,12 +41,14 @@ function writeJson(filePath, value) {
|
|||||||
export function normalizeClientSettings(input = {}) {
|
export function normalizeClientSettings(input = {}) {
|
||||||
return {
|
return {
|
||||||
homeBypassEnabled: Boolean(input.homeBypassEnabled),
|
homeBypassEnabled: Boolean(input.homeBypassEnabled),
|
||||||
|
proxyPort: normalizeProxyPort(input.proxyPort),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readClientSettings() {
|
export function readClientSettings() {
|
||||||
return normalizeClientSettings({
|
return normalizeClientSettings({
|
||||||
...DEFAULT_CLIENT_SETTINGS,
|
...DEFAULT_CLIENT_SETTINGS,
|
||||||
|
proxyPort: settings.proxyPort,
|
||||||
...readJson(settings.clientSettingsPath, {}),
|
...readJson(settings.clientSettingsPath, {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export const settings = {
|
|||||||
appMode: process.env.APP_MODE === "client" ? "client" : "gateway",
|
appMode: process.env.APP_MODE === "client" ? "client" : "gateway",
|
||||||
port: Number(process.env.PORT || 3456),
|
port: Number(process.env.PORT || 3456),
|
||||||
proxyPort: Number(process.env.PROXY_PORT || 8080),
|
proxyPort: Number(process.env.PROXY_PORT || 8080),
|
||||||
|
clientProxyPortStart: Number(process.env.CLIENT_PROXY_PORT_START || 8080),
|
||||||
|
clientProxyPortEnd: Number(process.env.CLIENT_PROXY_PORT_END || 8090),
|
||||||
tproxyPort: Number(process.env.TPROXY_PORT || 7895),
|
tproxyPort: Number(process.env.TPROXY_PORT || 7895),
|
||||||
bindIp: process.env.PROXY_BIND_IP || "0.0.0.0",
|
bindIp: process.env.PROXY_BIND_IP || "0.0.0.0",
|
||||||
dataDir,
|
dataDir,
|
||||||
|
|||||||
@@ -598,15 +598,21 @@ function publicState() {
|
|||||||
const state = readJson(settings.statePath, {});
|
const state = readJson(settings.statePath, {});
|
||||||
const customRules = readJson(settings.customRulesPath, []);
|
const customRules = readJson(settings.customRulesPath, []);
|
||||||
const deviceProfiles = readDeviceProfiles();
|
const deviceProfiles = readDeviceProfiles();
|
||||||
|
const clientSettings = readClientSettings();
|
||||||
const { subscriptionUrl, ...rest } = state;
|
const { subscriptionUrl, ...rest } = state;
|
||||||
return {
|
return {
|
||||||
mode: settings.appMode,
|
mode: settings.appMode,
|
||||||
port: settings.port,
|
port: settings.port,
|
||||||
proxyPort: settings.proxyPort,
|
proxyPort:
|
||||||
|
settings.appMode === "client" ? clientSettings.proxyPort : settings.proxyPort,
|
||||||
|
clientProxyPortRange: {
|
||||||
|
start: settings.clientProxyPortStart,
|
||||||
|
end: settings.clientProxyPortEnd,
|
||||||
|
},
|
||||||
proxyBindIp: settings.bindIp,
|
proxyBindIp: settings.bindIp,
|
||||||
tproxyPort: settings.appMode === "gateway" ? settings.tproxyPort : null,
|
tproxyPort: settings.appMode === "gateway" ? settings.tproxyPort : null,
|
||||||
routingRuDirect: settings.routingRuDirect,
|
routingRuDirect: settings.routingRuDirect,
|
||||||
clientSettings: readClientSettings(),
|
clientSettings,
|
||||||
configExists: fs.existsSync(settings.configPath),
|
configExists: fs.existsSync(settings.configPath),
|
||||||
singboxRunning: Boolean(singboxProcess),
|
singboxRunning: Boolean(singboxProcess),
|
||||||
singboxStartedAt,
|
singboxStartedAt,
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export function buildGatewayConfig(
|
|||||||
const clientOutbound = clientSettings?.homeBypassEnabled
|
const clientOutbound = clientSettings?.homeBypassEnabled
|
||||||
? "direct"
|
? "direct"
|
||||||
: vpnOutbound.tag;
|
: vpnOutbound.tag;
|
||||||
|
const mixedProxyPort = clientSettings?.proxyPort || settings.proxyPort;
|
||||||
const proxyOnlyRules = [{ inbound: [MIXED_INBOUND], outbound: clientOutbound }];
|
const proxyOnlyRules = [{ inbound: [MIXED_INBOUND], outbound: clientOutbound }];
|
||||||
const inbounds = [
|
const inbounds = [
|
||||||
...(clientMode
|
...(clientMode
|
||||||
@@ -282,7 +283,7 @@ export function buildGatewayConfig(
|
|||||||
type: "mixed",
|
type: "mixed",
|
||||||
tag: "mixed-in",
|
tag: "mixed-in",
|
||||||
listen: settings.bindIp,
|
listen: settings.bindIp,
|
||||||
listen_port: settings.proxyPort,
|
listen_port: mixedProxyPort,
|
||||||
sniff: true,
|
sniff: true,
|
||||||
set_system_proxy: false,
|
set_system_proxy: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { flagFor } from '../utils/country.js';
|
import { flagFor } from '../utils/country.js';
|
||||||
import { formatBytes, formatRelative } from '../utils/format.js';
|
import { formatBytes, formatRelative } from '../utils/format.js';
|
||||||
|
|
||||||
@@ -166,8 +166,21 @@ function ClientSetup({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProxyCard({ state }) {
|
function ProxyCard({ state, settings, busy, onSave }) {
|
||||||
const port = state?.proxyPort || 8080;
|
const range = state?.clientProxyPortRange || { start: 8080, end: 8090 };
|
||||||
|
const port = settings?.proxyPort || state?.proxyPort || 8080;
|
||||||
|
const [draftPort, setDraftPort] = useState(String(port));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraftPort(String(port));
|
||||||
|
}, [port]);
|
||||||
|
|
||||||
|
const parsedDraftPort = Number.parseInt(draftPort, 10);
|
||||||
|
const portInvalid =
|
||||||
|
!Number.isInteger(parsedDraftPort) ||
|
||||||
|
parsedDraftPort < range.start ||
|
||||||
|
parsedDraftPort > range.end;
|
||||||
|
const portDirty = !portInvalid && parsedDraftPort !== port;
|
||||||
const urls = useMemo(() => ({
|
const urls = useMemo(() => ({
|
||||||
http: `http://127.0.0.1:${port}`,
|
http: `http://127.0.0.1:${port}`,
|
||||||
socks: `socks5://127.0.0.1:${port}`,
|
socks: `socks5://127.0.0.1:${port}`,
|
||||||
@@ -179,6 +192,29 @@ function ProxyCard({ state }) {
|
|||||||
<h2>Локальный proxy</h2>
|
<h2>Локальный proxy</h2>
|
||||||
<span className="badge info">127.0.0.1:{port}</span>
|
<span className="badge info">127.0.0.1:{port}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field" style={{ marginBottom: 12 }}>
|
||||||
|
<label className="field-label">Порт proxy</label>
|
||||||
|
<div className="subscription-input">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
min={range.start}
|
||||||
|
max={range.end}
|
||||||
|
value={draftPort}
|
||||||
|
onChange={(e) => setDraftPort(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
disabled={busy || !portDirty}
|
||||||
|
onClick={() => onSave({ ...settings, proxyPort: parsedDraftPort })}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small className={portInvalid ? 'field-error' : 'field-hint'}>
|
||||||
|
Доступный диапазон: {range.start}–{range.end}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
<div className="copy-stack">
|
<div className="copy-stack">
|
||||||
<CopyField label="HTTP / HTTPS" value={urls.http} />
|
<CopyField label="HTTP / HTTPS" value={urls.http} />
|
||||||
<CopyField label="SOCKS5" value={urls.socks} />
|
<CopyField label="SOCKS5" value={urls.socks} />
|
||||||
@@ -187,8 +223,9 @@ function ProxyCard({ state }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeBypassCard({ settings, busy, onSave }) {
|
function HomeBypassCard({ state, settings, busy, onSave }) {
|
||||||
const enabled = Boolean(settings?.homeBypassEnabled);
|
const enabled = Boolean(settings?.homeBypassEnabled);
|
||||||
|
const port = settings?.proxyPort || state?.proxyPort || 8080;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
@@ -199,7 +236,7 @@ function HomeBypassCard({ settings, busy, onSave }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
Включайте дома: приложения продолжают использовать <code>127.0.0.1:8080</code>, но VPN не используется.
|
Включайте дома: приложения продолжают использовать <code>127.0.0.1:{port}</code>, но VPN не используется.
|
||||||
</p>
|
</p>
|
||||||
<label className="switch-row">
|
<label className="switch-row">
|
||||||
<span>
|
<span>
|
||||||
@@ -288,8 +325,13 @@ export function ClientOverviewPage({
|
|||||||
onApply={onApply}
|
onApply={onApply}
|
||||||
/>
|
/>
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<ProxyCard state={state} />
|
<ProxyCard
|
||||||
<HomeBypassCard settings={clientSettings} busy={busy} onSave={onSaveClientSettings} />
|
state={state}
|
||||||
|
settings={clientSettings}
|
||||||
|
busy={busy}
|
||||||
|
onSave={onSaveClientSettings}
|
||||||
|
/>
|
||||||
|
<HomeBypassCard state={state} settings={clientSettings} busy={busy} onSave={onSaveClientSettings} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<ClientActions
|
<ClientActions
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ process.env.SING_BOX_CACHE = path.join(process.env.DATA_DIR, "cache.db");
|
|||||||
const { buildGatewayConfig } = await import(
|
const { buildGatewayConfig } = await import(
|
||||||
`../../src/server/singbox.js?client-mode=${Date.now()}`
|
`../../src/server/singbox.js?client-mode=${Date.now()}`
|
||||||
);
|
);
|
||||||
|
const clientSettingsPath = path.join(process.env.DATA_DIR, "client-settings.json");
|
||||||
|
|
||||||
const subscriptionConfig = {
|
const subscriptionConfig = {
|
||||||
outbounds: [
|
outbounds: [
|
||||||
@@ -27,6 +28,7 @@ const subscriptionConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test("client mode exposes only the local mixed proxy inbound", () => {
|
test("client mode exposes only the local mixed proxy inbound", () => {
|
||||||
|
fs.rmSync(clientSettingsPath, { force: true });
|
||||||
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -38,6 +40,7 @@ test("client mode exposes only the local mixed proxy inbound", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("client mode routes mixed proxy fallback to the selected VPN", () => {
|
test("client mode routes mixed proxy fallback to the selected VPN", () => {
|
||||||
|
fs.rmSync(clientSettingsPath, { force: true });
|
||||||
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
||||||
|
|
||||||
assert.deepEqual(config.route.rule_set, []);
|
assert.deepEqual(config.route.rule_set, []);
|
||||||
@@ -47,8 +50,9 @@ test("client mode routes mixed proxy fallback to the selected VPN", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("client home bypass routes the local proxy directly", () => {
|
test("client home bypass routes the local proxy directly", () => {
|
||||||
|
fs.rmSync(clientSettingsPath, { force: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(process.env.DATA_DIR, "client-settings.json"),
|
clientSettingsPath,
|
||||||
JSON.stringify({ homeBypassEnabled: true }),
|
JSON.stringify({ homeBypassEnabled: true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -59,3 +63,18 @@ test("client home bypass routes the local proxy directly", () => {
|
|||||||
{ inbound: ["mixed-in"], outbound: "direct" },
|
{ inbound: ["mixed-in"], outbound: "direct" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("client mode uses selected proxy port from client settings", () => {
|
||||||
|
fs.rmSync(clientSettingsPath, { force: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
clientSettingsPath,
|
||||||
|
JSON.stringify({ proxyPort: 8085 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
||||||
|
|
||||||
|
assert.equal(config.inbounds[0].listen_port, 8085);
|
||||||
|
assert.deepEqual(config.route.rules, [
|
||||||
|
{ inbound: ["mixed-in"], outbound: "test-vpn" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user