diff --git a/.gitea/workflows/gateway-build.yml b/.gitea/workflows/gateway-build.yml index a6cb146..a99c138 100644 --- a/.gitea/workflows/gateway-build.yml +++ b/.gitea/workflows/gateway-build.yml @@ -111,7 +111,7 @@ jobs: DATAPLANE_IMAGE="${IMAGE}-dataplane:${{ gitea.sha }}" UPDATE_DATAPLANE=false if git diff-tree --no-commit-id --name-only -r -m HEAD | grep -Eq \ - '^(Dockerfile|entrypoint\.sh|package(-lock)?\.json|scripts/build-runtime-base\.sh|\.gitea/workflows/gateway-build\.yml|src/server/(config|dataplane|gatewayRouting|singboxRuntime|version)\.js|src/server/(adapters/neighbors|services/(deviceTrafficService|devicePolicyService))\.js|src/shared/errors\.js)$'; then + '^(Dockerfile|entrypoint\.sh|package(-lock)?\.json|scripts/build-runtime-base\.sh|\.gitea/workflows/gateway-build\.yml|src/server/(config|dataplane|gatewayRouting|singbox|singboxRuntime|version)\.js|src/server/(adapters/neighbors|services/(connectivityDiagnosticsService|deviceTrafficService|devicePolicyService))\.js|src/shared/errors\.js)$'; then UPDATE_DATAPLANE=true fi diff --git a/src/server/dataplaneClient.js b/src/server/dataplaneClient.js index effe185..0cc8354 100644 --- a/src/server/dataplaneClient.js +++ b/src/server/dataplaneClient.js @@ -1,7 +1,7 @@ import http from 'node:http'; import { HarborError } from '../shared/errors.js'; -function request(socketPath, pathname, method = 'GET', body = null) { +function request(socketPath, pathname, method = 'GET', body = null, timeoutMs = 6000) { return new Promise((resolve, reject) => { const encoded = body == null ? null : JSON.stringify(body); const req = http.request({ @@ -29,7 +29,7 @@ function request(socketPath, pathname, method = 'GET', body = null) { }); }); req.on('error', reject); - req.setTimeout(6000, () => req.destroy(new Error('Dataplane не ответил за 6 секунд'))); + req.setTimeout(timeoutMs, () => req.destroy(new Error(`Dataplane не ответил за ${Math.ceil(timeoutMs / 1000)} секунд`))); req.end(encoded); }); } @@ -56,7 +56,13 @@ export function createDataplaneClient(socketPath, send = request) { observeTraffic: () => send(socketPath, '/device-traffic', 'GET'), observeDevicePolicy: () => send(socketPath, '/device-policy', 'GET'), applyDevicePolicies: (devices) => send(socketPath, '/device-policy', 'PUT', { devices }), - runConnectivityDiagnostics: () => send(socketPath, '/diagnostics/connectivity', 'POST'), + runConnectivityDiagnostics: async () => { + try { + return await send(socketPath, '/diagnostics/connectivity', 'POST', null, 15_000); + } catch (cause) { + throw new HarborError('DIAGNOSTICS_FAILED', { cause }); + } + }, apply: () => update('/apply', 'POST'), restart: () => update('/restart', 'POST'), stop: () => update('/stop', 'POST'), diff --git a/src/shared/errors.js b/src/shared/errors.js index b6d36a8..f87faf7 100644 --- a/src/shared/errors.js +++ b/src/shared/errors.js @@ -13,6 +13,7 @@ export const ERROR_DEFINITIONS = Object.freeze({ DEVICE_NOT_FOUND: { status: 404, message: 'Устройство больше недоступно.', retryable: false }, DEVICE_IDENTITY_AMBIGUOUS: { status: 409, message: 'Gateway не может безопасно применить маршрут к этому устройству.', retryable: true }, DEVICE_POLICY_APPLY_FAILED: { status: 503, message: 'Не удалось применить маршрут устройства.', retryable: true }, + DIAGNOSTICS_FAILED: { status: 503, message: 'Не удалось проверить маршруты. Попробуйте ещё раз.', retryable: true }, CONFIG_INVALID: { status: 422, message: 'Конфигурация VPN недействительна.', retryable: false }, PROCESS_START_FAILED: { status: 503, message: 'Не удалось запустить VPN-процесс.', retryable: true }, OPERATION_IN_PROGRESS: { status: 409, message: 'Другая операция ещё выполняется.', retryable: true }, diff --git a/src/shared/versions.js b/src/shared/versions.js index 121f26d..71ecc44 100644 --- a/src/shared/versions.js +++ b/src/shared/versions.js @@ -1,7 +1,7 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.15.0', - gatewayClient: '0.16.0', - gatewayBackend: '0.16.0', + macClient: '0.15.1', + gatewayClient: '0.16.1', + gatewayBackend: '0.16.1', }); export function parseVersion(value) { diff --git a/test/server/dataplane-client.test.js b/test/server/dataplane-client.test.js index 958b046..19238f2 100644 --- a/test/server/dataplane-client.test.js +++ b/test/server/dataplane-client.test.js @@ -4,8 +4,8 @@ import { createDataplaneClient } from '../../src/server/dataplaneClient.js'; test('control uses the dataplane socket protocol', async () => { const requests = []; - const send = async (socketPath, pathname, method, body) => { - requests.push({ method, pathname, socketPath, body }); + const send = async (socketPath, pathname, method, body, timeoutMs) => { + requests.push({ method, pathname, socketPath, body, timeoutMs }); return { running: pathname !== '/stop', startedAt: 'now', @@ -42,4 +42,16 @@ test('control uses the dataplane socket protocol', async () => { 'POST /stop /run/dataplane.sock', ]); assert.deepEqual(requests[5].body, { devices: [{ id: 'dev_0011223344556677' }] }); + assert.equal(requests[6].timeoutMs, 15_000); +}); + +test('connectivity diagnostics expose a retryable domain error', async () => { + const client = createDataplaneClient('/run/dataplane.sock', async () => { + throw new Error('Dataplane не ответил'); + }); + + await assert.rejects(client.runConnectivityDiagnostics(), { + code: 'DIAGNOSTICS_FAILED', + retryable: true, + }); }); diff --git a/test/server/deploy-split.test.js b/test/server/deploy-split.test.js index 16c4089..8df6434 100644 --- a/test/server/deploy-split.test.js +++ b/test/server/deploy-split.test.js @@ -16,8 +16,8 @@ test('gateway deploy updates control without recreating dataplane', () => { assert.match(compose, /DATAPLANE_SOCKET: \/run\/vpn-proxy\/dataplane\.sock/); assert.match(deploy, /up -d --no-deps --wait[^\n]+vpn-proxy-control/); assert.match(workflow, /UPDATE_DATAPLANE="\$\{UPDATE_DATAPLANE\}"/); - assert.match(workflow, /src\/server\/\(config\|dataplane\|gatewayRouting\|singboxRuntime\|version\)/); - assert.match(workflow, /src\/server\/\(adapters\/neighbors\|services\/\(deviceTrafficService\|devicePolicyService\)\)/); + assert.match(workflow, /src\/server\/\(config\|dataplane\|gatewayRouting\|singbox\|singboxRuntime\|version\)/); + assert.match(workflow, /src\/server\/\(adapters\/neighbors\|services\/\(connectivityDiagnosticsService\|deviceTrafficService\|devicePolicyService\)\)/); assert.match(workflow, /src\/shared\/errors/); assert.doesNotMatch(workflow, /dataplaneClient/); });