Remove legacy vpn proxy code and simplify the client
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
async function withEnv(patch, fn) {
|
||||
const previous = {};
|
||||
for (const key of Object.keys(patch)) {
|
||||
previous[key] = process.env[key];
|
||||
if (patch[key] === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = patch[key];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("client proxy range defaults to the single configured proxy port", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
PROXY_PORT: "8082",
|
||||
CLIENT_PROXY_PORT_START: "8082",
|
||||
CLIENT_PROXY_PORT_END: undefined,
|
||||
},
|
||||
async () => {
|
||||
const { settings } = await import(
|
||||
`../../src/server/config.js?single-proxy-port=${Date.now()}`
|
||||
);
|
||||
|
||||
assert.equal(settings.proxyPort, 8082);
|
||||
assert.equal(settings.clientProxyPortStart, 8082);
|
||||
assert.equal(settings.clientProxyPortEnd, 8082);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("client proxy defaults to 8082 when no port is configured", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
APP_MODE: "client",
|
||||
PROXY_PORT: undefined,
|
||||
CLIENT_PROXY_PORT_START: undefined,
|
||||
CLIENT_PROXY_PORT_END: undefined,
|
||||
},
|
||||
async () => {
|
||||
const { settings } = await import(
|
||||
`../../src/server/config.js?client-default-port=${Date.now()}`
|
||||
);
|
||||
|
||||
assert.equal(settings.proxyPort, 8082);
|
||||
assert.equal(settings.clientProxyPortStart, 8082);
|
||||
assert.equal(settings.clientProxyPortEnd, 8082);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,167 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const {
|
||||
deviceCidrs,
|
||||
normalizeDeviceProfiles,
|
||||
} = await import("../../src/server/devices.js");
|
||||
const { matchRoute } = await import("../../src/server/routeMatcher.js");
|
||||
const {
|
||||
sourceBypassCidrs,
|
||||
buildSourceBypassIptablesCommands,
|
||||
} = await import("../../src/server/tproxySourceBypass.js");
|
||||
const { settings } = await import("../../src/server/config.js");
|
||||
|
||||
test("default source bypass chain name fits iptables chain length limit", () => {
|
||||
assert.equal(settings.tproxySourceBypassChain, "VPN_PROXY_SRC_BYPASS");
|
||||
assert.equal(settings.tproxySourceForwardChain, "VPN_PROXY_FWD_BYPASS");
|
||||
assert.equal(settings.tproxySourceNatChain, "VPN_PROXY_NAT_BYPASS");
|
||||
assert.ok(settings.tproxySourceBypassChain.length <= 28);
|
||||
assert.ok(settings.tproxySourceForwardChain.length <= 28);
|
||||
assert.ok(settings.tproxySourceNatChain.length <= 28);
|
||||
});
|
||||
|
||||
test("device profiles preserve bypass mode for kernel-level TProxy bypass", () => {
|
||||
const profiles = normalizeDeviceProfiles({
|
||||
devices: [
|
||||
{
|
||||
id: "pc",
|
||||
name: "PC",
|
||||
enabled: true,
|
||||
ip: "192.168.50.25",
|
||||
mode: "bypass",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(profiles.devices[0].mode, "bypass");
|
||||
assert.deepEqual(deviceCidrs(profiles.devices, "bypass"), [
|
||||
"192.168.50.25/32",
|
||||
]);
|
||||
});
|
||||
|
||||
test("route checker reports transparent bypass before sing-box rules", () => {
|
||||
const result = matchRoute(
|
||||
{
|
||||
host: "example.com",
|
||||
ip: "93.184.216.34",
|
||||
sourceIp: "192.168.50.25",
|
||||
inbound: "tproxy-in",
|
||||
},
|
||||
[
|
||||
{
|
||||
id: "vpn-all",
|
||||
enabled: true,
|
||||
name: "VPN all",
|
||||
domains: ["example.com"],
|
||||
outbound: "vpn",
|
||||
},
|
||||
],
|
||||
{
|
||||
vpnTag: "test-vpn",
|
||||
deviceProfiles: {
|
||||
defaultTransparentMode: "vpn",
|
||||
proxyDefaultMode: "vpn",
|
||||
devices: [
|
||||
{
|
||||
id: "pc",
|
||||
name: "PC",
|
||||
enabled: true,
|
||||
ip: "192.168.50.25",
|
||||
mode: "bypass",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.matched, "kernel-bypass");
|
||||
assert.equal(result.ruleName, "PC -> bypass TProxy");
|
||||
assert.equal(result.outbound, "direct");
|
||||
});
|
||||
|
||||
test("source bypass sync combines env CIDRs and bypass-mode devices", () => {
|
||||
const cidrs = sourceBypassCidrs(
|
||||
{
|
||||
devices: [
|
||||
{ enabled: true, ip: "192.168.50.25", mode: "bypass" },
|
||||
{ enabled: false, ip: "192.168.50.26", mode: "bypass" },
|
||||
{ enabled: true, ip: "192.168.50.27", mode: "direct" },
|
||||
],
|
||||
},
|
||||
"192.168.50.30/32",
|
||||
);
|
||||
|
||||
assert.deepEqual(cidrs, ["192.168.50.30/32", "192.168.50.25/32"]);
|
||||
});
|
||||
|
||||
test("source bypass iptables commands use ACCEPT inside the managed subchain", () => {
|
||||
assert.deepEqual(
|
||||
buildSourceBypassIptablesCommands(["192.168.50.25/32"], {
|
||||
chain: "VPN_PROXY_SOURCE_BYPASS",
|
||||
forwardChain: "VPN_PROXY_FWD_BYPASS",
|
||||
natChain: "VPN_PROXY_NAT_BYPASS",
|
||||
natBypassCidrs: ["10.0.0.0/8"],
|
||||
}),
|
||||
[
|
||||
["-w", "-t", "mangle", "-F", "VPN_PROXY_SOURCE_BYPASS"],
|
||||
["-w", "-F", "VPN_PROXY_FWD_BYPASS"],
|
||||
["-w", "-t", "nat", "-F", "VPN_PROXY_NAT_BYPASS"],
|
||||
[
|
||||
"-w",
|
||||
"-t",
|
||||
"mangle",
|
||||
"-A",
|
||||
"VPN_PROXY_SOURCE_BYPASS",
|
||||
"-s",
|
||||
"192.168.50.25/32",
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
],
|
||||
[
|
||||
"-w",
|
||||
"-A",
|
||||
"VPN_PROXY_FWD_BYPASS",
|
||||
"-s",
|
||||
"192.168.50.25/32",
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
],
|
||||
[
|
||||
"-w",
|
||||
"-A",
|
||||
"VPN_PROXY_FWD_BYPASS",
|
||||
"-d",
|
||||
"192.168.50.25/32",
|
||||
"-m",
|
||||
"conntrack",
|
||||
"--ctstate",
|
||||
"RELATED,ESTABLISHED",
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
],
|
||||
[
|
||||
"-w",
|
||||
"-t",
|
||||
"nat",
|
||||
"-A",
|
||||
"VPN_PROXY_NAT_BYPASS",
|
||||
"-d",
|
||||
"10.0.0.0/8",
|
||||
"-j",
|
||||
"RETURN",
|
||||
],
|
||||
[
|
||||
"-w",
|
||||
"-t",
|
||||
"nat",
|
||||
"-A",
|
||||
"VPN_PROXY_NAT_BYPASS",
|
||||
"-s",
|
||||
"192.168.50.25/32",
|
||||
"-j",
|
||||
"MASQUERADE",
|
||||
],
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -1,122 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, "../..");
|
||||
const ENTRYPOINT = path.join(ROOT, "entrypoint.sh");
|
||||
|
||||
function writeExecutable(filePath, contents) {
|
||||
fs.writeFileSync(filePath, contents, { mode: 0o755 });
|
||||
}
|
||||
|
||||
test("entrypoint bypasses configured source CIDRs before TProxy interception", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "vpn-entrypoint-"));
|
||||
const commandLog = path.join(tmp, "commands.log");
|
||||
|
||||
writeExecutable(
|
||||
path.join(tmp, "iptables"),
|
||||
`#!/usr/bin/env bash
|
||||
printf 'iptables %s\\n' "$*" >> "$COMMAND_LOG"
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
writeExecutable(
|
||||
path.join(tmp, "ip"),
|
||||
`#!/usr/bin/env bash
|
||||
printf 'ip %s\\n' "$*" >> "$COMMAND_LOG"
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
writeExecutable(
|
||||
path.join(tmp, "ipset"),
|
||||
`#!/usr/bin/env bash
|
||||
printf 'ipset %s\\n' "$*" >> "$COMMAND_LOG"
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
writeExecutable(
|
||||
path.join(tmp, "sysctl"),
|
||||
`#!/usr/bin/env bash
|
||||
printf 'sysctl %s\\n' "$*" >> "$COMMAND_LOG"
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
writeExecutable(
|
||||
path.join(tmp, "node"),
|
||||
`#!/usr/bin/env bash
|
||||
printf 'node %s\\n' "$*" >> "$COMMAND_LOG"
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
|
||||
const result = spawnSync("bash", [ENTRYPOINT], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${tmp}${path.delimiter}${process.env.PATH}`,
|
||||
COMMAND_LOG: commandLog,
|
||||
TPROXY_BYPASS_SOURCE_CIDRS: "192.168.50.25/32 192.168.50.26/32",
|
||||
DIRECT_BYPASS_CACHE: "true",
|
||||
BYPASS_CIDRS: "10.0.0.0/8",
|
||||
},
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
|
||||
const commands = fs.readFileSync(commandLog, "utf8").trim().split("\n");
|
||||
const sourceBypassIndex = commands.findIndex((line) =>
|
||||
line.includes(
|
||||
"iptables -w -t mangle -A VPN_PROXY_SRC_BYPASS -s 192.168.50.25/32 -j ACCEPT",
|
||||
),
|
||||
);
|
||||
const secondSourceBypassIndex = commands.findIndex((line) =>
|
||||
line.includes(
|
||||
"iptables -w -t mangle -A VPN_PROXY_SRC_BYPASS -s 192.168.50.26/32 -j ACCEPT",
|
||||
),
|
||||
);
|
||||
const sourceBypassJumpIndex = commands.findIndex((line) =>
|
||||
line.includes(
|
||||
"iptables -w -t mangle -A VPN_PROXY_TPROXY -j VPN_PROXY_SRC_BYPASS",
|
||||
),
|
||||
);
|
||||
const directCacheIndex = commands.findIndex((line) =>
|
||||
line.includes("-m set --match-set vpn_direct_bypass dst -j RETURN"),
|
||||
);
|
||||
const tproxyIndex = commands.findIndex((line) =>
|
||||
line.includes("-p tcp -j TPROXY --on-port 7895"),
|
||||
);
|
||||
const ipForwardIndex = commands.findIndex((line) =>
|
||||
line.includes("sysctl -w net.ipv4.ip_forward=1"),
|
||||
);
|
||||
const forwardAcceptIndex = commands.findIndex((line) =>
|
||||
line.includes(
|
||||
"iptables -w -A VPN_PROXY_FWD_BYPASS -s 192.168.50.25/32 -j ACCEPT",
|
||||
),
|
||||
);
|
||||
const forwardReturnIndex = commands.findIndex((line) =>
|
||||
line.includes(
|
||||
"iptables -w -A VPN_PROXY_FWD_BYPASS -d 192.168.50.25/32 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT",
|
||||
),
|
||||
);
|
||||
const natMasqueradeIndex = commands.findIndex((line) =>
|
||||
line.includes(
|
||||
"iptables -w -t nat -A VPN_PROXY_NAT_BYPASS -s 192.168.50.25/32 -j MASQUERADE",
|
||||
),
|
||||
);
|
||||
|
||||
assert.notEqual(sourceBypassIndex, -1);
|
||||
assert.notEqual(secondSourceBypassIndex, -1);
|
||||
assert.notEqual(sourceBypassJumpIndex, -1);
|
||||
assert.notEqual(directCacheIndex, -1);
|
||||
assert.notEqual(tproxyIndex, -1);
|
||||
assert.notEqual(ipForwardIndex, -1);
|
||||
assert.notEqual(forwardAcceptIndex, -1);
|
||||
assert.notEqual(forwardReturnIndex, -1);
|
||||
assert.notEqual(natMasqueradeIndex, -1);
|
||||
assert.ok(sourceBypassJumpIndex < directCacheIndex);
|
||||
assert.ok(sourceBypassJumpIndex < tproxyIndex);
|
||||
});
|
||||
15
test/server/entrypoint-tproxy.test.js
Normal file
15
test/server/entrypoint-tproxy.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const entrypoint = fs.readFileSync(
|
||||
path.resolve(import.meta.dirname, '../../entrypoint.sh'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
test('gateway intercepts all public TCP and UDP traffic without source bypasses', () => {
|
||||
assert.match(entrypoint, /-p tcp -j TPROXY --on-port "\$TPROXY_PORT"/);
|
||||
assert.match(entrypoint, /-p udp -j TPROXY --on-port "\$TPROXY_PORT"/);
|
||||
assert.doesNotMatch(entrypoint, /TPROXY_BYPASS_SOURCE_CIDRS|DIRECT_BYPASS_CACHE|ipset/);
|
||||
});
|
||||
@@ -1,131 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
process.env.APP_MODE = "client";
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "vpn-proxy-test-"));
|
||||
process.env.SING_BOX_CACHE = path.join(process.env.DATA_DIR, "cache.db");
|
||||
process.env.APP_MODE = 'client';
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'vpn-proxy-client-test-'));
|
||||
process.env.SING_BOX_CACHE = path.join(process.env.DATA_DIR, 'cache.db');
|
||||
|
||||
const { buildGatewayConfig } = await import(
|
||||
`../../src/server/singbox.js?client-mode=${Date.now()}`
|
||||
);
|
||||
const clientSettingsPath = path.join(process.env.DATA_DIR, "client-settings.json");
|
||||
const { buildGatewayConfig } = await import(`../../src/server/singbox.js?client=${Date.now()}`);
|
||||
|
||||
const subscriptionConfig = {
|
||||
outbounds: [
|
||||
{
|
||||
type: "vless",
|
||||
tag: "test-vpn",
|
||||
server: "vpn.example.test",
|
||||
server_port: 443,
|
||||
uuid: "00000000-0000-4000-8000-000000000000",
|
||||
tls: { enabled: true },
|
||||
},
|
||||
],
|
||||
customRules: [],
|
||||
outbounds: [{
|
||||
type: 'vless',
|
||||
tag: 'test-vpn',
|
||||
server: 'vpn.example.test',
|
||||
server_port: 443,
|
||||
uuid: '00000000-0000-4000-8000-000000000000',
|
||||
tls: { enabled: true },
|
||||
}],
|
||||
};
|
||||
|
||||
test("client mode exposes only the local mixed proxy inbound", () => {
|
||||
fs.rmSync(clientSettingsPath, { force: true });
|
||||
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
||||
test('client exposes one local proxy and routes it through the selected VPN', () => {
|
||||
const config = buildGatewayConfig(subscriptionConfig, 'test-vpn');
|
||||
|
||||
assert.deepEqual(
|
||||
config.inbounds.map((inbound) => inbound.tag),
|
||||
["mixed-in"],
|
||||
);
|
||||
assert.equal(config.inbounds[0].type, "mixed");
|
||||
assert.deepEqual(config.inbounds.map((inbound) => inbound.tag), ['mixed-in']);
|
||||
assert.equal(config.inbounds[0].listen_port, 8082);
|
||||
});
|
||||
|
||||
test("client mode routes mixed proxy fallback to the selected VPN", () => {
|
||||
fs.rmSync(clientSettingsPath, { force: true });
|
||||
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
||||
|
||||
assert.deepEqual(config.route.rule_set, []);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ['mixed-in'], outbound: 'test-vpn' },
|
||||
]);
|
||||
assert.equal(config.route.final, 'test-vpn');
|
||||
assert.equal(config.route.auto_detect_interface, undefined);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ["mixed-in"], outbound: "test-vpn" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("client home bypass routes the local proxy directly", () => {
|
||||
fs.rmSync(clientSettingsPath, { force: true });
|
||||
fs.writeFileSync(
|
||||
clientSettingsPath,
|
||||
JSON.stringify({ homeBypassEnabled: true }),
|
||||
);
|
||||
|
||||
const config = buildGatewayConfig(subscriptionConfig, "test-vpn");
|
||||
|
||||
assert.deepEqual(config.route.rule_set, []);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ["mixed-in"], outbound: "direct" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("client home bypass can build direct proxy without local VPN", () => {
|
||||
fs.rmSync(clientSettingsPath, { force: true });
|
||||
fs.writeFileSync(
|
||||
clientSettingsPath,
|
||||
JSON.stringify({ homeBypassEnabled: true }),
|
||||
);
|
||||
|
||||
const config = buildGatewayConfig({ outbounds: [], customRules: [] }, "");
|
||||
|
||||
assert.deepEqual(config.outbounds, [
|
||||
{ type: "direct", tag: "direct" },
|
||||
{ type: "block", tag: "block" },
|
||||
]);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ["mixed-in"], outbound: "direct" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("client mode ignores saved proxy port outside the published single port", () => {
|
||||
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, 8082);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ["mixed-in"], outbound: "test-vpn" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("client shared proxy mode routes local proxy to gateway socks outbound", () => {
|
||||
fs.rmSync(clientSettingsPath, { force: true });
|
||||
fs.writeFileSync(
|
||||
clientSettingsPath,
|
||||
JSON.stringify({
|
||||
sharedProxyEnabled: true,
|
||||
sharedProxy: {
|
||||
host: "192.168.50.111",
|
||||
port: 8080,
|
||||
protocol: "socks5",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const config = buildGatewayConfig({ outbounds: [], customRules: [] }, "");
|
||||
|
||||
assert.deepEqual(config.inbounds.map((inbound) => inbound.tag), ["mixed-in"]);
|
||||
assert.deepEqual(
|
||||
config.outbounds.find((outbound) => outbound.tag === "shared-proxy"),
|
||||
{
|
||||
type: "socks",
|
||||
tag: "shared-proxy",
|
||||
server: "192.168.50.111",
|
||||
server_port: 8080,
|
||||
version: "5",
|
||||
},
|
||||
);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ["mixed-in"], outbound: "shared-proxy" },
|
||||
]);
|
||||
});
|
||||
|
||||
33
test/server/singbox-gateway-mode.test.js
Normal file
33
test/server/singbox-gateway-mode.test.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
process.env.APP_MODE = 'gateway';
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'vpn-proxy-gateway-test-'));
|
||||
process.env.SING_BOX_CACHE = path.join(process.env.DATA_DIR, 'cache.db');
|
||||
|
||||
const { buildGatewayConfig } = await import(`../../src/server/singbox.js?gateway=${Date.now()}`);
|
||||
|
||||
const subscriptionConfig = {
|
||||
outbounds: [{
|
||||
type: 'vless',
|
||||
tag: 'test-vpn',
|
||||
server: 'vpn.example.test',
|
||||
server_port: 443,
|
||||
uuid: '00000000-0000-4000-8000-000000000000',
|
||||
tls: { enabled: true },
|
||||
}],
|
||||
};
|
||||
|
||||
test('gateway routes transparent and proxy traffic only through the selected VPN', () => {
|
||||
const config = buildGatewayConfig(subscriptionConfig, 'test-vpn');
|
||||
|
||||
assert.deepEqual(config.route.rule_set, []);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{ inbound: ['tproxy-in'], outbound: 'test-vpn' },
|
||||
{ inbound: ['mixed-in'], outbound: 'test-vpn' },
|
||||
]);
|
||||
assert.equal(config.route.final, 'test-vpn');
|
||||
});
|
||||
Reference in New Issue
Block a user