Files
EasyTier/easytier-web/frontend-lib/scripts/test-network-config.mjs
T
KKRainbow 03078b4791 feat(dhcp): support automatic IPv6 address assignment (#2688)
Accept a boolean or an IPv6 CIDR in dhcpv6 and --dhcpv6, using
fd6b:8c2d:91e4::/64 when no initial subnet is supplied. Keep IPv4
configuration serialization compatible and DHCPv6 disabled by default.

Share the DHCP lifecycle and host update between both address families,
with typed allocation state, peer subnet selection, conflict recovery,
and bounded IPv6 probing. Preserve static addresses of the other family
and prevent static config patches from replacing DHCP-owned leases.

Carry IPv6 settings, leases, and events through management, web config,
and mobile VPN setup. Reuse the shared NIC registry and mobile address
list for IPv6-only and dual-stack operation without extra FD owners.

Cover config round trips, allocation and recovery, mobile
reconciliation, three-node IPv6 and dual-stack connectivity, and shared
TUN member exit. Validate with workspace clippy, Docker TUN tests,
frontend tests/builds, WASI and Android Rust checks, and Android VPN
Kotlin compilation.
2026-10-14 00:36:47 +08:00

608 lines
18 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import ts from 'typescript'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(__dirname, '..')
const generatedApiManagePath = path.join(projectRoot, 'src/generated/proto/api_manage.ts')
const distPath = path.join(projectRoot, 'dist/easytier-frontend-lib.js')
const { NetworkTypes } = await import(pathToFileURL(distPath))
const {
AclAction,
AclChainType,
AclProtocol,
CompressionAlgoPb,
DEFAULT_NETWORK_CONFIG,
NetworkingMethod,
normalizeNetworkConfig,
normalizeVpnPortalInfo,
toBackendNetworkConfig,
} = NetworkTypes
const BOOLEAN_CONFIG_FIELDS = [
'dhcp',
'dhcpv6',
'advanced_settings',
'latency_first',
'use_smoltcp',
'disable_ipv6',
'enable_kcp_proxy',
'disable_kcp_input',
'disable_p2p',
'bind_device',
'no_tun',
'enable_exit_node',
'relay_all_peer_rpc',
'multi_thread',
'enable_relay_network_whitelist',
'enable_manual_routes',
'proxy_forward_by_system',
'disable_encryption',
'enable_socks5',
'disable_udp_hole_punching',
'enable_magic_dns',
'enable_private_mode',
'enable_quic_proxy',
'disable_quic_input',
'disable_sym_hole_punching',
'p2p_only',
'lazy_p2p',
'need_p2p',
'disable_upnp',
'ipv6_public_addr_provider',
'ipv6_public_addr_auto',
'disable_relay_data',
'enable_udp_broadcast_relay',
'disable_tcp_hole_punching',
]
const LEGACY_VPN_PORTAL_FIELDS = [
'enable_vpn_portal',
'vpn_portal_listen_port',
'vpn_portal_client_network_addr',
'vpn_portal_client_network_len',
]
function readGeneratedNetworkConfigFields() {
const source = ts.createSourceFile(
generatedApiManagePath,
fs.readFileSync(generatedApiManagePath, 'utf8'),
ts.ScriptTarget.Latest,
true,
)
for (const statement of source.statements) {
if (!ts.isInterfaceDeclaration(statement) || statement.name.text !== 'NetworkConfig') {
continue
}
return statement.members
.filter(ts.isPropertySignature)
.map((member) => member.name.getText(source).replace(/^['"]|['"]$/g, ''))
}
throw new Error(`NetworkConfig interface not found in ${generatedApiManagePath}`)
}
function expectNoCamelCaseKeys(value, pathSegments = []) {
if (!value || typeof value !== 'object') {
return
}
if (Array.isArray(value)) {
value.forEach((item, index) => expectNoCamelCaseKeys(item, [...pathSegments, String(index)]))
return
}
for (const [key, child] of Object.entries(value)) {
assert.equal(
/[A-Z]/.test(key),
false,
`JSON key should use proto field name: ${[...pathSegments, key].join('.')}`,
)
expectNoCamelCaseKeys(child, [...pathSegments, key])
}
}
function allFieldFixture() {
return {
...DEFAULT_NETWORK_CONFIG(),
instance_id: '11111111-2222-3333-4444-555555555555',
dhcp: false,
dhcp_ipv4_cidr: '10.9.8.0/25',
dhcpv6: true,
dhcp_ipv6_cidr: 'fd12::/64',
virtual_ipv4: '10.9.8.7',
network_length: 25,
hostname: 'frontend-e2e',
network_name: 'full-field-network',
network_secret: 'full-field-secret',
networking_method: NetworkingMethod.Manual,
public_server_url: 'tcp://public.example:11010',
peer_urls: [' tcp://peer-a:11010 ', '', 'udp://peer-b:11010'],
peers: [
{
uri: 'tcp://peer-a:11010',
peer_public_key: 'peer-a-public-key',
},
],
proxy_cidrs: ['10.10.0.0/16', '192.168.2.0/24->10.99.0.0/24'],
vpn_portal_config: {
wireguard_listen: '0.0.0.0:23000',
wireguard_private_key: 'portal-private-key',
clients: [{
name: 'phone-a',
virtual_ip: '10.9.8.10',
groups: ['ops'],
}],
},
advanced_settings: true,
listener_urls: ['tcp://0.0.0.0:12010', 'udp://0.0.0.0:12010'],
latency_first: true,
dev_name: 'et-full',
use_smoltcp: true,
disable_ipv6: true,
enable_kcp_proxy: true,
disable_kcp_input: true,
disable_p2p: true,
bind_device: false,
no_tun: true,
enable_exit_node: true,
relay_all_peer_rpc: true,
multi_thread: false,
enable_relay_network_whitelist: true,
relay_network_whitelist: ['10.0.0.0/8', 'fd00::/8'],
enable_manual_routes: true,
routes: ['10.20.0.0/16', 'fd00:20::/64'],
exit_nodes: ['10.9.8.1', 'fd00::1'],
proxy_forward_by_system: true,
disable_encryption: true,
enable_socks5: true,
socks5_port: 1081,
disable_udp_hole_punching: true,
mtu: 1280,
mapped_listeners: ['tcp://127.0.0.1:13010'],
enable_magic_dns: true,
enable_private_mode: true,
enable_quic_proxy: true,
disable_quic_input: true,
quic_listen_port: 14010,
port_forwards: [
{
proto: 'tcp',
bind_ip: '127.0.0.1',
bind_port: 8080,
dst_ip: '10.9.8.7',
dst_port: 80,
},
{
proto: 'udp',
bind_ip: '0.0.0.0',
bind_port: 5353,
dst_ip: '10.9.8.8',
dst_port: 53,
},
],
disable_sym_hole_punching: true,
p2p_only: true,
data_compress_algo: CompressionAlgoPb.Zstd,
encryption_algorithm: 'aes-gcm',
disable_tcp_hole_punching: true,
secure_mode: {
enabled: true,
local_private_key: 'private-key',
local_public_key: 'public-key',
},
acl: {
acl_v1: {
group: {
declares: [
{
group_name: 'ops',
group_secret: 'ops-secret',
},
],
members: ['node-a', 'node-b'],
},
chains: [
{
name: 'forward-chain',
chain_type: AclChainType.Forward,
description: 'forward traffic',
enabled: true,
default_action: AclAction.Drop,
rules: [
{
name: 'allow-web',
description: 'allow web traffic',
priority: 100,
enabled: true,
protocol: AclProtocol.TCP,
ports: ['80', '443'],
source_ips: ['10.0.0.0/8'],
destination_ips: ['10.9.8.7/32'],
source_ports: ['1024-65535'],
action: AclAction.Allow,
rate_limit: 1000,
burst_limit: 2000,
stateful: true,
source_groups: ['ops'],
destination_groups: ['web'],
},
],
},
],
},
},
credential_file: '/tmp/easytier-credential.toml',
managed_credentials: [
{
credential_id: 'managed-credential',
credential_secret: 'managed-secret',
groups: ['ops'],
allow_relay: true,
allowed_proxy_cidrs: ['10.30.0.0/16'],
expiry_unix: '1791971218',
reusable: true,
},
],
lazy_p2p: true,
need_p2p: true,
prefer_peer_relay: true,
instance_recv_bps_limit: '9007199254740993',
disable_upnp: true,
ipv6_public_addr_provider: true,
ipv6_public_addr_auto: true,
ipv6_public_addr_prefix: '2001:db8:1::/64',
disable_relay_data: true,
enable_udp_broadcast_relay: true,
socket_mark: 1234,
}
}
function assertFixtureCoversGeneratedFields() {
const generatedFields = readGeneratedNetworkConfigFields()
.filter((field) => !LEGACY_VPN_PORTAL_FIELDS.includes(field))
const fixtureFields = new Set(Object.keys(allFieldFixture()))
const missing = generatedFields.filter((field) => !fixtureFields.has(field))
assert.deepEqual(missing, [], 'all generated NetworkConfig fields should be represented in the fixture')
}
function assertFullFieldRoundTrip() {
const input = allFieldFixture()
const normalized = normalizeNetworkConfig(input)
assert.equal(normalized.peer_urls.join(','), 'tcp://peer-a:11010,udp://peer-b:11010')
assert.equal(normalized.instance_recv_bps_limit, '9007199254740993')
assert.equal(normalized.data_compress_algo, CompressionAlgoPb.Zstd)
assert.equal(normalized.acl.acl_v1.chains[0].chain_type, AclChainType.Forward)
assert.equal(normalized.acl.acl_v1.chains[0].rules[0].protocol, AclProtocol.TCP)
const backend = toBackendNetworkConfig(normalized)
expectNoCamelCaseKeys(backend)
for (const field of readGeneratedNetworkConfigFields()
.filter((field) => !LEGACY_VPN_PORTAL_FIELDS.includes(field))) {
assert.ok(field in backend, `backend JSON should include fixture field ${field}`)
}
assert.equal(backend.networking_method, 'Manual')
assert.equal(backend.public_server_url, '')
assert.deepEqual(backend.peer_urls, ['tcp://peer-a:11010', 'udp://peer-b:11010'])
assert.equal(backend.peers[0].peer_public_key, 'peer-a-public-key')
assert.deepEqual(backend.peers[1], { uri: 'udp://peer-b:11010' })
assert.equal(backend.data_compress_algo, 'Zstd')
assert.equal(backend.instance_recv_bps_limit, '9007199254740993')
assert.equal(backend.vpn_portal_config.wireguard_listen, '0.0.0.0:23000')
assert.equal(backend.vpn_portal_config.clients[0].name, 'phone-a')
assert.deepEqual(backend.vpn_portal_config.clients[0].groups, ['ops'])
assert.equal(backend.secure_mode.enabled, true)
assert.equal(backend.secure_mode.local_private_key, 'private-key')
assert.equal(backend.acl.acl_v1.chains[0].chain_type, 'Forward')
assert.equal(backend.acl.acl_v1.chains[0].default_action, 'Drop')
assert.equal(backend.acl.acl_v1.chains[0].rules[0].protocol, 'TCP')
assert.equal(backend.acl.acl_v1.chains[0].rules[0].action, 'Allow')
assert.equal(backend.port_forwards[1].proto, 'udp')
assert.equal(backend.socket_mark, 1234)
}
function assertLegacyVpnPortalFieldsReachBackendValidation() {
const backend = toBackendNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
enable_vpn_portal: true,
vpn_portal_listen_port: 22022,
vpn_portal_client_network_addr: '10.88.0.0',
vpn_portal_client_network_len: 24,
})
assert.equal(backend.enable_vpn_portal, true)
assert.equal(backend.vpn_portal_listen_port, 22022)
assert.equal(backend.vpn_portal_client_network_addr, '10.88.0.0')
assert.equal(backend.vpn_portal_client_network_len, 24)
}
function assertVpnPortalRpcJsonNormalization() {
const info = normalizeVpnPortalInfo({
vpn_type: 'wireguard',
listener: '0.0.0.0:22022',
clients: [{
name: 'phone-a',
virtual_ip: '10.9.8.10',
groups: ['ops'],
state: 'VPN_PORTAL_CLIENT_STATE_ONLINE',
client_config: '[Interface]',
}],
})
assert.equal(info.clients[0].state, 3)
assert.deepEqual(info.connected_clients, [])
}
function assertBooleanFieldValuesPreserved() {
const input = allFieldFixture()
const normalized = normalizeNetworkConfig(input)
const backend = toBackendNetworkConfig(normalized)
for (const field of BOOLEAN_CONFIG_FIELDS) {
assert.equal(
normalized[field],
input[field],
`normalized config should preserve boolean field ${field}`,
)
assert.equal(
backend[field],
input[field],
`backend JSON should preserve boolean field ${field}`,
)
}
}
function assertEnumCompatibility() {
const normalized = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
networking_method: 'Manual',
data_compress_algo: 'Zstd',
acl: {
acl_v1: {
group: { declares: [], members: [] },
chains: [
{
chain_type: 'Forward',
default_action: 'Drop',
rules: [
{
protocol: 'TCP',
action: 'Allow',
},
],
},
],
},
},
})
assert.equal(normalized.data_compress_algo, CompressionAlgoPb.Zstd)
assert.equal(normalized.acl.acl_v1.chains[0].chain_type, AclChainType.Forward)
assert.equal(normalized.acl.acl_v1.chains[0].default_action, AclAction.Drop)
assert.equal(normalized.acl.acl_v1.chains[0].rules[0].protocol, AclProtocol.TCP)
assert.equal(normalized.acl.acl_v1.chains[0].rules[0].action, AclAction.Allow)
const backend = toBackendNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
data_compress_algo: 'Zstd',
acl: {
acl_v1: {
group: { declares: [], members: [] },
chains: [
{
chain_type: 'Forward',
default_action: 'Drop',
rules: [
{
protocol: 'TCP',
action: 'Allow',
},
],
},
],
},
},
})
assert.equal(backend.data_compress_algo, 'Zstd')
assert.equal(backend.acl.acl_v1.chains[0].chain_type, 'Forward')
assert.equal(backend.acl.acl_v1.chains[0].rules[0].protocol, 'TCP')
}
function assertAclDefaultsAndExplicitZero() {
const partialAcl = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
acl: {
acl_v1: {
group: { declares: [], members: [] },
chains: [{ rules: [{}] }],
},
},
})
const defaultedChain = partialAcl.acl.acl_v1.chains[0]
assert.equal(defaultedChain.chain_type, AclChainType.UnspecifiedChain)
assert.equal(defaultedChain.default_action, AclAction.Allow)
assert.equal(defaultedChain.rules[0].protocol, AclProtocol.Any)
assert.equal(defaultedChain.rules[0].action, AclAction.Allow)
const explicitZero = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
acl: {
acl_v1: {
group: { declares: [], members: [] },
chains: [
{
chain_type: 0,
default_action: 0,
rules: [{ protocol: 0, action: 0 }],
},
],
},
},
})
const zeroChain = explicitZero.acl.acl_v1.chains[0]
assert.equal(zeroChain.chain_type, AclChainType.UnspecifiedChain)
assert.equal(zeroChain.default_action, AclAction.Noop)
assert.equal(zeroChain.rules[0].protocol, AclProtocol.Unspecified)
assert.equal(zeroChain.rules[0].action, AclAction.Noop)
}
function assertNetworkingMethodNormalization() {
const publicServer = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
networking_method: 'PublicServer',
public_server_url: ' tcp://public.example:11010 ',
peer_urls: ['tcp://manual.example:11010'],
})
assert.equal(publicServer.networking_method, NetworkingMethod.Manual)
assert.equal(publicServer.public_server_url, '')
assert.deepEqual(publicServer.peer_urls, ['tcp://public.example:11010'])
const standalone = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
networking_method: 'Standalone',
peer_urls: ['tcp://manual.example:11010'],
})
assert.equal(standalone.networking_method, NetworkingMethod.Manual)
assert.deepEqual(standalone.peer_urls, [])
const missing = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
networking_method: undefined,
peer_urls: [' tcp://one ', '', 'udp://two '],
})
assert.deepEqual(missing.peer_urls, ['tcp://one', 'udp://two'])
const publicServerMissingUrl = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
networking_method: 'PublicServer',
public_server_url: '',
peer_urls: ['tcp://manual.example:11010'],
})
assert.deepEqual(publicServerMissingUrl.peer_urls, [])
}
function assertPeerPublicKeysPreserved() {
const normalized = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
peer_urls: [],
peers: [
{
uri: ' tcp://peer-a:11010 ',
peer_public_key: 'peer-a-public-key',
},
],
})
assert.deepEqual(normalized.peer_urls, ['tcp://peer-a:11010'])
assert.deepEqual(normalized.peers, [
{
uri: 'tcp://peer-a:11010',
peer_public_key: 'peer-a-public-key',
},
])
const unchangedUrl = toBackendNetworkConfig({
...normalized,
peer_urls: ['tcp://peer-a:11010', 'tcp://peer-b:11010'],
})
assert.equal(unchangedUrl.peers[0].peer_public_key, 'peer-a-public-key')
assert.deepEqual(unchangedUrl.peers[1], { uri: 'tcp://peer-b:11010' })
const changedUrl = toBackendNetworkConfig({
...normalized,
peer_urls: ['tcp://peer-c:11010'],
})
assert.deepEqual(changedUrl.peers, [{ uri: 'tcp://peer-c:11010' }])
const clearedUrls = toBackendNetworkConfig({
...normalized,
peer_urls: [],
})
assert.deepEqual(clearedUrls.peer_urls ?? [], [])
assert.deepEqual(clearedUrls.peers ?? [], [])
}
function assertNumberBoundaries() {
const safeLimit = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
instance_recv_bps_limit: '12345',
})
assert.equal(safeLimit.instance_recv_bps_limit, 12345)
const largeLimit = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
instance_recv_bps_limit: '9007199254740993',
})
assert.equal(largeLimit.instance_recv_bps_limit, '9007199254740993')
assert.equal(toBackendNetworkConfig(largeLimit).instance_recv_bps_limit, '9007199254740993')
const invalidNumbers = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
mtu: Number.NaN,
instance_recv_bps_limit: Number.POSITIVE_INFINITY,
})
assert.equal(invalidNumbers.mtu, null)
assert.equal(invalidNumbers.instance_recv_bps_limit, null)
const emptyLimit = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
instance_recv_bps_limit: '',
})
assert.equal(emptyLimit.instance_recv_bps_limit, null)
const zeroLimit = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
instance_recv_bps_limit: '0',
})
assert.equal(zeroLimit.instance_recv_bps_limit, null)
assert.equal(toBackendNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
instance_recv_bps_limit: 0,
}).instance_recv_bps_limit, undefined)
const oversizedLimit = normalizeNetworkConfig({
...DEFAULT_NETWORK_CONFIG(),
instance_recv_bps_limit: '18446744073709551616',
})
assert.equal(oversizedLimit.instance_recv_bps_limit, null)
}
const tests = [
assertFixtureCoversGeneratedFields,
assertFullFieldRoundTrip,
assertLegacyVpnPortalFieldsReachBackendValidation,
assertVpnPortalRpcJsonNormalization,
assertBooleanFieldValuesPreserved,
assertEnumCompatibility,
assertAclDefaultsAndExplicitZero,
assertNetworkingMethodNormalization,
assertPeerPublicKeysPreserved,
assertNumberBoundaries,
]
for (const test of tests) {
test()
console.log(`ok ${test.name}`)
}