Update Harbor client and gateway functionality
This commit is contained in:
@@ -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) ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user