Update Harbor client and gateway functionality
Build and Deploy Gateway / build-and-push (push) Successful in 35s
Build and Deploy Gateway / deploy (push) Successful in 14s

This commit is contained in:
2026-08-17 15:23:16 +03:00
parent 0b39211fbd
commit 7c255192b0
41 changed files with 1443 additions and 218 deletions
+34 -5
View File
@@ -1,4 +1,8 @@
import { normalizeRouteRules } from '../routingRules.js';
import {
normalizeRouteRules,
ROUTE_RULES_CONTRACT_VERSION,
type RouteRuleOutbound,
} from '../routingRules.js';
import { normalizeServers, resolveServerId } from '../serverIdentity.js';
export type HarborMode = 'client' | 'gateway';
@@ -18,6 +22,7 @@ export interface RouteRule {
type: 'domain' | 'domain_suffix' | 'domain_keyword';
value: string;
enabled: boolean;
outbound: RouteRuleOutbound;
}
export interface StoredProfile {
@@ -74,6 +79,7 @@ export interface StateSnapshot {
lastError: string | null;
};
route: {
rulesContractVersion?: typeof ROUTE_RULES_CONTRACT_VERSION;
mode: string;
gatewayAddress: string | null;
gatewayUiOrigin: string | null;
@@ -343,7 +349,8 @@ export function createStateSnapshot({
: routeMode === 'gateway-direct'
? gatewayAuto?.failures ? 'gateway-stale' : 'gateway-found'
: gatewayAuto?.lastError ? 'gateway-lost' : 'local';
const activeLocalRules = running ? stored.appliedRouteRules : [];
const localRulesBypassed = mode === 'client' && routeMode === 'gateway-direct';
const activeLocalRules = running && !localRulesBypassed ? stored.appliedRouteRules : [];
const appliedServerSnapshot = stored.appliedServerSnapshot;
return assertStateSnapshot({
@@ -377,6 +384,7 @@ export function createStateSnapshot({
lastError: null,
},
route: {
rulesContractVersion: ROUTE_RULES_CONTRACT_VERSION,
mode: routeMode,
gatewayAddress: mode === 'client' ? gatewayAuto?.gateway?.gateway || null : null,
gatewayUiOrigin: mode === 'client' ? gatewayAuto?.uiOrigin || null : null,
@@ -387,7 +395,9 @@ export function createStateSnapshot({
localRules: stored.routeRules,
activeLocalRules,
localRulesRevision: stored.routeRulesRevision,
localRulesPendingRestart: !isSameRules(stored.routeRules, activeLocalRules),
localRulesPendingRestart: localRulesBypassed
? false
: !isSameRules(stored.routeRules, activeLocalRules),
},
operation: {
kind: nullableText(operation.kind),
@@ -402,7 +412,24 @@ export function createStateSnapshot({
}
export function assertStateSnapshot(snapshot: unknown): StateSnapshot {
const candidate = snapshot as StateSnapshot;
const rawCandidate = snapshot as StateSnapshot;
const legacyRule = (rule: RouteRule): RouteRule => (
rule && !Object.hasOwn(rule, 'outbound')
? { ...rule, outbound: 'direct' }
: rule
);
const candidate = rawCandidate?.route?.rulesContractVersion === undefined
&& Array.isArray(rawCandidate?.route?.localRules)
&& Array.isArray(rawCandidate?.route?.activeLocalRules)
? {
...rawCandidate,
route: {
...rawCandidate.route,
localRules: rawCandidate.route.localRules.map(legacyRule),
activeLocalRules: rawCandidate.route.activeLocalRules.map(legacyRule),
},
}
: rawCandidate;
const validDate = (value: unknown) => typeof value === 'string' && Number.isFinite(Date.parse(value));
const nullableDate = (value: unknown) => value === null || validDate(value);
const nullableString = (value: unknown) => value === null || typeof value === 'string';
@@ -420,7 +447,8 @@ export function assertStateSnapshot(snapshot: unknown): StateSnapshot {
['domain', 'domain_suffix', 'domain_keyword'].includes(rule.type) &&
typeof rule.value === 'string' &&
Boolean(rule.value) &&
typeof rule.enabled === 'boolean'
typeof rule.enabled === 'boolean' &&
['vpn', 'direct'].includes(rule.outbound)
);
const validProfile = (profile: ProfileSnapshot) => (
profile &&
@@ -469,6 +497,7 @@ export function assertStateSnapshot(snapshot: unknown): StateSnapshot {
!nullableDate(candidate.connection.startedAt) ||
!nullableString(candidate.connection.lastError) ||
!candidate.route ||
![undefined, ROUTE_RULES_CONTRACT_VERSION].includes(candidate.route.rulesContractVersion) ||
typeof candidate.route.mode !== 'string' ||
!nullableString(candidate.route.gatewayAddress) ||
!nullableString(candidate.route.gatewayUiOrigin) ||
+20 -4
View File
@@ -1,16 +1,20 @@
export const INITIAL_ROUTE_RULES = Object.freeze([
Object.freeze({ type: 'domain_suffix', value: 'ru', enabled: true }),
Object.freeze({ type: 'domain_suffix', value: 'ru', enabled: true, outbound: 'direct' }),
]);
const RULE_TYPES = new Set(['domain', 'domain_suffix', 'domain_keyword']);
const RULE_OUTBOUNDS = new Set(['vpn', 'direct']);
export const ROUTE_RULES_CONTRACT_VERSION = 2;
export const MAX_ROUTE_RULES = 200;
export type RouteRuleType = 'domain' | 'domain_suffix' | 'domain_keyword';
export type RouteRuleOutbound = 'vpn' | 'direct';
export interface NormalizedRouteRule {
type: RouteRuleType;
value: string;
enabled: boolean;
outbound: RouteRuleOutbound;
}
function record(value: unknown): Record<string, unknown> {
@@ -28,20 +32,32 @@ function hostname(value: unknown) {
return normalized;
}
function normalizeRule(input: unknown): NormalizedRouteRule {
function normalizeRule(input: unknown, strict: boolean): NormalizedRouteRule {
const rule = record(input);
const type = String(rule.type || '').trim();
if (!RULE_TYPES.has(type)) throw new TypeError('Invalid domain rule type');
if (Object.hasOwn(rule, 'enabled') && typeof rule.enabled !== 'boolean') {
throw new TypeError('Invalid domain rule enabled state');
}
if (strict && !Object.hasOwn(rule, 'outbound')) {
throw new TypeError('Route rule outbound is required');
}
const outbound = Object.hasOwn(rule, 'outbound')
? String(rule.outbound || '').trim()
: 'direct';
if (!RULE_OUTBOUNDS.has(outbound)) throw new TypeError('Invalid route rule outbound');
const value = type === 'domain_keyword'
? String(rule.value || '').trim().toLowerCase()
: hostname(rule.value);
if (!value || value.length > 253 || /[\s/:?#]/.test(value)) {
throw new TypeError('Invalid domain rule value');
}
return { type: type as RouteRuleType, value, enabled: rule.enabled !== false };
return {
type: type as RouteRuleType,
value,
enabled: rule.enabled !== false,
outbound: outbound as RouteRuleOutbound,
};
}
export function normalizeRouteRules(
@@ -60,7 +76,7 @@ export function normalizeRouteRules(
const normalized: NormalizedRouteRule[] = [];
for (const candidate of value.slice(0, MAX_ROUTE_RULES)) {
try {
const rule = normalizeRule(candidate);
const rule = normalizeRule(candidate, strict);
const key = `${rule.type}:${rule.value}`;
if (seen.has(key)) continue;
seen.add(key);
+3 -3
View File
@@ -1,7 +1,7 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.25.16',
gatewayClient: '0.26.15',
gatewayBackend: '0.26.5',
macClient: '0.26.0',
gatewayClient: '0.27.0',
gatewayBackend: '0.27.0',
});
export interface ParsedVersion {