#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import { accessSync, constants, readFileSync } from 'node:fs'; import net from 'node:net'; import { userInfo } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(SCRIPT_DIR, '..'); const DEFAULT_SERVICE_UNIT = path.join( REPO_ROOT, 'deploy/systemd/genarrative-pingora-gateway.service', ); const DEFAULT_DROPIN_TEMPLATE = path.join( REPO_ROOT, 'deploy/systemd/genarrative-pingora-gateway-direct-entry.conf', ); const DEFAULT_ENV_EXAMPLE = path.join( REPO_ROOT, 'deploy/pingora/pingora-gateway.env.example', ); const DEFAULT_SYSTEMD_SERVICE = 'genarrative-pingora-gateway.service'; const DEFAULT_SERVICE_USER = 'genarrative'; const config = parseArgs(process.argv.slice(2)); const failures = []; const warnings = []; await main(); if (warnings.length > 0) { for (const warning of warnings) { console.warn(`[pingora-direct-preflight] WARNING ${warning}`); } } if (failures.length > 0) { console.error('[pingora-direct-preflight] FAILED'); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } console.log('[pingora-direct-preflight] OK'); function usage() { console.log(`Usage: node scripts/check-pingora-direct-preflight.mjs [options] 默认只检查仓库内模板,适合本机提交前护栏。 Options: --env-file 目标机 /etc/genarrative/pingora-gateway.env。 --require-live-env 强制 env 文件包含 TLS_LISTEN / HTTP_REDIRECT_LISTEN / cert / key。 --check-cert-readable 检查 env 中 cert/key 文件当前用户可读;目标机切换窗口建议启用。 --check-service-env-file 检查 service EnvironmentFile 包含本次 --env-file。 --check-service-user-cert-readable 检查 systemd 服务用户可读 env 中 cert/key;目标机切换窗口必须启用。 --check-service-binary-executable 检查 service ExecStart 指向的 pingora-gateway 已存在且可执行。 --check-ports-free 检查 env 中 TLS_LISTEN / HTTP_REDIRECT_LISTEN 端口当前可绑定。 --allow-loopback-only 只允许直连入口监听 127.0.0.1 / ::1;本机高端口 smoke 可用。 --systemd-cat 调用 systemctl cat 检查已生效 service。 --systemd-service systemd service 名,默认 genarrative-pingora-gateway.service。 --service-user 检查 cert/key 可读性时使用的 systemd 服务用户;默认从 service 模板 User= 推导。 --service-unit 仓库内主 service 模板路径。 --dropin-template 仓库内 direct-entry drop-in 模板路径。 --env-example 仓库内 env 示例路径。 --json 输出 JSON 结果。 Environment aliases: GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_REQUIRE_LIVE_ENV GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SERVICE_USER GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT `); } function parseArgs(argv) { const result = { envFile: process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE || '', requireLiveEnv: parseBoolEnv( process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_REQUIRE_LIVE_ENV, false, 'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_REQUIRE_LIVE_ENV', ), checkCertReadable: parseBoolEnv( process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE, false, 'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_CERT_READABLE', ), checkServiceEnvFile: parseBoolEnv( process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE, false, 'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_ENV_FILE', ), checkServiceUserCertReadable: parseBoolEnv( process.env .GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE, false, 'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE', ), checkServiceBinaryExecutable: parseBoolEnv( process.env .GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE, false, 'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE', ), serviceUser: process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SERVICE_USER || '', checkPortsFree: parseBoolEnv( process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE, false, 'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_CHECK_PORTS_FREE', ), systemdCat: parseBoolEnv( process.env.GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT, false, 'GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SYSTEMD_CAT', ), allowLoopbackOnly: false, systemdService: DEFAULT_SYSTEMD_SERVICE, serviceUnit: DEFAULT_SERVICE_UNIT, dropinTemplate: DEFAULT_DROPIN_TEMPLATE, envExample: DEFAULT_ENV_EXAMPLE, json: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; switch (arg) { case '-h': case '--help': usage(); process.exit(0); break; case '--env-file': result.envFile = requireValue(argv, ++index, arg); break; case '--require-live-env': result.requireLiveEnv = true; break; case '--check-cert-readable': result.checkCertReadable = true; break; case '--check-service-env-file': result.checkServiceEnvFile = true; break; case '--check-service-user-cert-readable': result.checkServiceUserCertReadable = true; break; case '--check-service-binary-executable': result.checkServiceBinaryExecutable = true; break; case '--check-ports-free': result.checkPortsFree = true; break; case '--allow-loopback-only': result.allowLoopbackOnly = true; break; case '--systemd-cat': result.systemdCat = true; break; case '--systemd-service': result.systemdService = requireValue(argv, ++index, arg); break; case '--service-user': result.serviceUser = requireValue(argv, ++index, arg); break; case '--service-unit': result.serviceUnit = requireValue(argv, ++index, arg); break; case '--dropin-template': result.dropinTemplate = requireValue(argv, ++index, arg); break; case '--env-example': result.envExample = requireValue(argv, ++index, arg); break; case '--json': result.json = true; break; default: throw new Error(`未知参数: ${arg}`); } } if ( (result.requireLiveEnv || result.checkCertReadable || result.checkServiceEnvFile || result.checkServiceUserCertReadable || result.checkPortsFree) && !result.envFile ) { throw new Error( '--require-live-env / --check-cert-readable / --check-service-env-file / --check-service-user-cert-readable / --check-ports-free 需要同时提供 --env-file', ); } if (result.envFile) { validateSafeAbsoluteFilePath(result.envFile, '--env-file'); } return result; } function requireValue(argv, index, flag) { const value = argv[index]; if (!value || value.startsWith('--')) { throw new Error(`${flag} 缺少参数值`); } return value; } function parseBoolEnv(raw, fallback, label) { if (raw === undefined || raw === null || String(raw).trim() === '') { return fallback; } const normalized = String(raw).trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(normalized)) { return true; } if (['0', 'false', 'no', 'off'].includes(normalized)) { return false; } throw new Error(`${label} 必须是布尔值 true/false 或 1/0。`); } function validateSafeAbsoluteFilePath(value, flag) { validateNoControlCharacters(value, flag); if (!path.isAbsolute(value)) { throw new Error(`${flag} 必须是绝对路径。`); } if (isFilesystemRootPath(value)) { throw new Error(`${flag} 不能是文件系统根目录。`); } } function validateNoControlCharacters(value, label) { if (/[\0\r\n]/u.test(String(value))) { throw new Error(`${label} 不能包含换行或 NUL 字符。`); } } function isFilesystemRootPath(value) { const resolved = path.resolve(String(value)); return resolved === path.parse(resolved).root; } async function main() { const serviceTemplate = readFile(config.serviceUnit); const dropinTemplate = readFile(config.dropinTemplate); const envExample = readFile(config.envExample); if (!config.serviceUser) { config.serviceUser = resolveServiceUser(serviceTemplate); } assertTemplateShape(serviceTemplate, dropinTemplate, envExample); if (config.checkServiceEnvFile) { assertServiceEnvFile(serviceTemplate, config.serviceUnit); } if (config.checkServiceBinaryExecutable) { assertServiceBinaryExecutable(serviceTemplate); } let env = new Map(); if (config.envFile) { env = parseEnvFile(config.envFile); await assertLiveEnv(env); } else if (config.requireLiveEnv) { failures.push('缺少 --env-file,无法验证目标机直连 env。'); } if (config.systemdCat) { const systemdCatContent = assertSystemdCat(); if (config.checkServiceEnvFile && systemdCatContent) { assertServiceEnvFile( systemdCatContent, `systemctl cat ${config.systemdService}`, ); } } if (config.json) { console.log( JSON.stringify( { ok: failures.length === 0, warnings, envFile: config.envFile, tlsListen: env.get('GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN') || '', httpRedirectListen: env.get('GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN') || '', systemdCat: config.systemdCat, checkServiceEnvFile: config.checkServiceEnvFile, checkServiceUserCertReadable: config.checkServiceUserCertReadable, checkServiceBinaryExecutable: config.checkServiceBinaryExecutable, serviceUser: config.serviceUser, }, null, 2, ), ); } } function readFile(file) { try { return readFileSync(file, 'utf8'); } catch (error) { failures.push(`无法读取 ${file}: ${error.message}`); return ''; } } function assertTemplateShape(serviceTemplate, dropinTemplate, envExample) { assertIncludes( config.serviceUnit, serviceTemplate, '影子网关只监听本机高端口', '主 service 必须保留 shadow 说明。', ); assertExcludes( config.serviceUnit, serviceTemplate, 'AmbientCapabilities=CAP_NET_BIND_SERVICE', '主 service 不能默认授予低端口 capability。', ); assertIncludes( config.dropinTemplate, dropinTemplate, 'AmbientCapabilities=CAP_NET_BIND_SERVICE', 'direct-entry drop-in 必须授予低端口绑定 capability。', ); assertIncludes( config.dropinTemplate, dropinTemplate, 'CapabilityBoundingSet=CAP_NET_BIND_SERVICE', 'direct-entry drop-in 必须限制 capability 边界。', ); assertIncludes( config.dropinTemplate, dropinTemplate, '/etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf', 'direct-entry drop-in 必须写明人工启用位置。', ); assertIncludes( config.envExample, envExample, 'CAP_NET_BIND_SERVICE', 'env 示例必须提示低端口需要 systemd drop-in。', ); } function assertIncludes(file, content, needle, reason) { if (!content.includes(needle)) { failures.push(`${file} 缺少 ${needle}。${reason}`); } } function assertExcludes(file, content, needle, reason) { if (content.includes(needle)) { failures.push(`${file} 不应包含 ${needle}。${reason}`); } } function parseEnvFile(file) { const content = readFile(file); const env = new Map(); for (const rawLine of content.split(/\r?\n/u)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) { continue; } const separator = line.indexOf('='); if (separator <= 0) { continue; } const key = line.slice(0, separator).trim(); const value = unquoteEnvValue(line.slice(separator + 1).trim()); env.set(key, value); } return env; } function unquoteEnvValue(value) { if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { return value.slice(1, -1); } return value; } async function assertLiveEnv(env) { const tlsListen = requireEnv(env, 'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN'); const httpListen = requireEnv( env, 'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN', ); const certFile = requireEnv(env, 'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE'); const keyFile = requireEnv(env, 'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE'); const proto = requireEnv(env, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO'); validateEnvValueNoControlCharacters( tlsListen, 'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN', ); validateEnvValueNoControlCharacters( httpListen, 'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN', ); validateEnvValueNoControlCharacters( certFile, 'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE', ); validateEnvValueNoControlCharacters( keyFile, 'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE', ); validateEnvValueNoControlCharacters( proto, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO', ); if (proto && proto !== 'https') { failures.push( `GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO 应为 https,实际 ${proto}`, ); } const tlsEndpoint = parseListenAddress( tlsListen, 'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN', ); const httpEndpoint = parseListenAddress( httpListen, 'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN', ); if ( tlsEndpoint && httpEndpoint && endpointsEqual(tlsEndpoint, httpEndpoint) ) { failures.push('TLS_LISTEN 和 HTTP_REDIRECT_LISTEN 不能复用同一监听地址。'); } assertForwardedForTrust(env, [tlsEndpoint, httpEndpoint].filter(Boolean)); if (config.allowLoopbackOnly) { for (const endpoint of [tlsEndpoint, httpEndpoint].filter(Boolean)) { if (!isLoopbackHost(endpoint.host)) { failures.push( `${endpoint.label}=${endpoint.raw} 不是 loopback 地址;--allow-loopback-only 下不允许。`, ); } } } if (config.checkCertReadable) { assertReadableFile(certFile, 'TLS_CERT_FILE'); assertReadableFile(keyFile, 'TLS_KEY_FILE'); } if (config.checkServiceUserCertReadable) { assertReadableFileAsServiceUser(certFile, 'TLS_CERT_FILE'); assertReadableFileAsServiceUser(keyFile, 'TLS_KEY_FILE'); } if (config.checkPortsFree) { for (const endpoint of [tlsEndpoint, httpEndpoint].filter(Boolean)) { await assertPortBindable(endpoint); } } assertProtectionInstanceBoundary(env); } function requireEnv(env, key) { const value = env.get(key) || ''; if (!value && config.requireLiveEnv) { failures.push(`${config.envFile} 缺少 ${key}`); } return value; } function validateEnvValueNoControlCharacters(value, label) { if (!value) { return; } try { validateNoControlCharacters(value, label); } catch (error) { failures.push(error.message); } } function assertForwardedForTrust(env, endpoints) { const trustForwardedFor = parseBoolValue( env.get('GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR'), 'GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR', ); if (!trustForwardedFor) { return; } const confirmed = parseBoolValue( env.get('GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED'), 'GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED', ); if (!confirmed) { failures.push( 'GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true 时必须同时设置 GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true。', ); } const publicEndpoints = endpoints.filter( (endpoint) => !isLoopbackHost(endpoint.host), ); if (publicEndpoints.length > 0) { failures.push( `公网直连 Pingora 时 GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR 必须保持 false,当前监听 ${publicEndpoints.map((endpoint) => `${endpoint.label}=${endpoint.raw}`).join(', ')}。`, ); } } function assertProtectionInstanceBoundary(env) { const protectionEnabled = parseBoolValue( env.get('GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED'), 'GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED', true, ); const instanceCount = parseOptionalPositiveIntValue( env.get('GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT'), 1, 'GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT', ); const sharedProtectionConfirmed = parseBoolValue( env.get('GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED'), 'GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED', ); if ( protectionEnabled && instanceCount > 1 && !sharedProtectionConfirmed ) { failures.push( 'Pingora 接流保护当前默认是进程内状态;GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1 且 PROTECTION_ENABLED=true 时,必须设置 GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true,或关闭网关保护并由前置层承担。', ); } } function parseBoolValue(raw, label, fallback = false) { if (raw === undefined || raw === null || String(raw).trim() === '') { return fallback; } const normalized = String(raw).trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(normalized)) { return true; } if (['0', 'false', 'no', 'off'].includes(normalized)) { return false; } failures.push(`${label} 必须是布尔值 true/false 或 1/0。`); return fallback; } function parseOptionalPositiveIntValue(raw, fallback, label) { if (raw === undefined || raw === null || String(raw).trim() === '') { return fallback; } const text = String(raw).trim(); if (!/^[1-9]\d*$/u.test(text)) { failures.push(`${label} 必须是正整数。`); return fallback; } return Number.parseInt(text, 10); } function parseListenAddress(raw, label) { if (!raw) { return null; } const lastColon = raw.lastIndexOf(':'); if (lastColon <= 0 || lastColon === raw.length - 1) { failures.push(`${label}=${raw} 不是 host:port 格式。`); return null; } let host = raw.slice(0, lastColon); const port = Number.parseInt(raw.slice(lastColon + 1), 10); if (host.startsWith('[') && host.endsWith(']')) { host = host.slice(1, -1); } if (!Number.isFinite(port) || port <= 0 || port > 65535) { failures.push(`${label}=${raw} 端口无效。`); return null; } return { label, raw, host, port }; } function endpointsEqual(left, right) { return left.host === right.host && left.port === right.port; } function isLoopbackHost(host) { return ['127.0.0.1', '::1', 'localhost'].includes(host); } function assertReadableFile(file, label) { if (!file) { return; } try { accessSync(file, constants.R_OK); } catch (error) { failures.push(`${label} 当前用户不可读: ${file} (${error.message})`); } } function assertExecutableFile(file, label) { if (!file) { return; } try { accessSync(file, constants.X_OK); } catch (error) { failures.push(`${label} 不存在或不可执行: ${file} (${error.message})`); } } function assertReadableFileAsServiceUser(file, label) { if (!file) { return; } const user = config.serviceUser || DEFAULT_SERVICE_USER; try { validateNoControlCharacters(user, '--service-user'); validateNoControlCharacters(file, label); } catch (error) { failures.push(error.message); return; } const currentUser = currentUsername(); if (currentUser === user) { assertReadableFile(file, `${label} 服务用户 ${user}`); return; } const result = runCommand('sudo', ['-n', '-u', user, 'test', '-r', file]); if (result.controlCharacterError) { failures.push(result.controlCharacterError); return; } if (result.error) { failures.push( `${label} 无法以服务用户 ${user} 检查可读性: ${result.error.message};请在目标机安装 sudo,或以 ${user} 用户运行 preflight。`, ); return; } if (result.status !== 0) { const detail = (result.stderr || result.stdout || '').trim(); failures.push( `${label} 服务用户 ${user} 不可读: ${file}${detail ? ` (${detail})` : ''}`, ); } } function runCommand(command, args) { try { validateNoControlCharacters(command, '子命令可执行文件'); for (const arg of args) { validateNoControlCharacters(arg, '子命令参数'); } } catch (error) { return { status: 1, stdout: '', stderr: '', error: null, controlCharacterError: error.message, }; } return spawnSync(command, args, { encoding: 'utf8', }); } function assertServiceEnvFile(serviceTemplate, sourceLabel) { const envFiles = resolveServiceEnvFiles(serviceTemplate); const expected = path.resolve(config.envFile); if (envFiles.length === 0) { failures.push( `${sourceLabel} 缺少 EnvironmentFile,无法确认 service 会读取本次 preflight env。`, ); return; } if (!envFiles.some((envFile) => path.resolve(envFile) === expected)) { failures.push( `${sourceLabel} EnvironmentFile 未包含本次 --env-file: ${config.envFile};实际 ${envFiles.join(', ')}`, ); } } function resolveServiceEnvFiles(serviceTemplate) { const envFiles = []; for (const rawLine of serviceTemplate.split(/\r?\n/u)) { const line = rawLine.trim(); if (!line.startsWith('EnvironmentFile=')) { continue; } const rawValue = line.slice('EnvironmentFile='.length).trim(); if (!rawValue) { envFiles.length = 0; continue; } for (const word of splitSystemdWords(rawValue)) { const value = word.startsWith('-') ? word.slice(1).trim() : word; if (value) { envFiles.push(value); } } } return envFiles; } function splitSystemdWords(value) { const words = []; let current = ''; let quote = ''; for (let index = 0; index < value.length; index += 1) { const char = value[index]; if (quote) { if (char === quote) { quote = ''; } else { current += char; } continue; } if (char === '"' || char === "'") { quote = char; continue; } if (/\s/u.test(char)) { if (current) { words.push(current); current = ''; } continue; } current += char; } if (current) { words.push(current); } return words; } function resolveServiceUser(serviceTemplate) { const userLine = serviceTemplate .split(/\r?\n/u) .map((line) => line.trim()) .find((line) => line.startsWith('User=')); if (!userLine) { return DEFAULT_SERVICE_USER; } const user = userLine.slice('User='.length).trim(); return user || DEFAULT_SERVICE_USER; } function assertServiceBinaryExecutable(serviceTemplate) { const execStart = resolveServiceExecStart(serviceTemplate); if (!execStart) { failures.push(`${config.serviceUnit} 缺少 ExecStart,无法检查网关二进制。`); return; } const binaryPath = firstExecWord(execStart); if (!binaryPath || !path.isAbsolute(binaryPath)) { failures.push( `${config.serviceUnit} ExecStart 不是绝对可执行文件路径: ${execStart}`, ); return; } assertExecutableFile(binaryPath, 'Pingora service ExecStart'); } function resolveServiceExecStart(serviceTemplate) { const execLine = serviceTemplate .split(/\r?\n/u) .map((line) => line.trim()) .find((line) => line.startsWith('ExecStart=')); if (!execLine) { return ''; } return execLine.slice('ExecStart='.length).trim(); } function firstExecWord(execStart) { const trimmed = execStart.trim(); if (!trimmed) { return ''; } if (trimmed.startsWith('"')) { const closingQuote = trimmed.indexOf('"', 1); return closingQuote > 1 ? trimmed.slice(1, closingQuote) : ''; } return trimmed.split(/\s+/u)[0] || ''; } function currentUsername() { if (process.env.USER || process.env.LOGNAME) { return process.env.USER || process.env.LOGNAME; } try { return userInfo().username; } catch { return ''; } } function assertPortBindable(endpoint) { return new Promise((resolve) => { const server = net.createServer(); let settled = false; function finish(error) { if (settled) { return; } settled = true; if (error) { failures.push( `${endpoint.label}=${endpoint.raw} 当前不可绑定: ${error.message}`, ); resolve(); } else { console.log( `[pingora-direct-preflight] ${endpoint.label}=${endpoint.raw} 当前端口可绑定`, ); server.close(() => resolve()); } } server.once('error', finish); server.listen( { host: endpoint.host, port: endpoint.port, exclusive: true, }, () => finish(null), ); }); } function assertSystemdCat() { const result = runCommand('systemctl', ['cat', config.systemdService]); if (result.controlCharacterError) { failures.push(result.controlCharacterError); return ''; } if (result.error) { failures.push(`systemctl cat 启动失败: ${result.error.message}`); return ''; } if (result.status !== 0) { failures.push( `systemctl cat ${config.systemdService} 失败: ${(result.stderr || result.stdout || '').trim()}`, ); return ''; } const content = result.stdout || ''; assertIncludes( `systemctl cat ${config.systemdService}`, content, 'AmbientCapabilities=CAP_NET_BIND_SERVICE', '目标机直连低端口前必须启用 direct-entry drop-in。', ); assertIncludes( `systemctl cat ${config.systemdService}`, content, 'CapabilityBoundingSet=CAP_NET_BIND_SERVICE', '目标机 direct-entry drop-in 必须限制 capability 边界。', ); return content; }