import { createHash, X509Certificate } from "node:crypto"; import { readFile, writeFile, mkdir, mkdtemp, rename, rm, copyFile, stat } from "node:fs/promises"; import { spawnSync } from "node:child_process"; import path from "node:path"; import { integrationAccessUrl } from "../packages/config/dist/index.js"; import { PRIVATE_NETWORK_TOOLS, privateNetworkRoot, privateGatewayPorts } from "../apps/agent/dist/private-gateway.js"; export async function provisionPrivateNetwork(config) { if (!config.server.privateGateway) return; if (process.platform !== "win32" || process.arch !== "x64") throw new Error("Private gateway provisioning currently supports Windows x64."); const root = privateNetworkRoot(); await mkdir(root, { recursive: true }); for (const tool of PRIVATE_NETWORK_TOOLS) { const target = path.join(root, `${tool.name}-${tool.version}.exe`); try { if (!(await stat(target)).isFile()) throw new Error("Tool target must be a regular file"); continue; } catch (error) { if (error.code !== "ENOENT") throw error; } const staging = await mkdtemp(path.join(root, "download-")); try { const response = await fetch(`https://api.github.com/repos/${tool.repository}/releases/assets/${tool.assetId}`, { headers: { Accept: "application/octet-stream", "User-Agent": "PalmTTY-setup" }, signal: AbortSignal.timeout(600_000) }); if (!response.ok) throw new Error(`Could not download official ${tool.name} archive: ${response.status}`); const chunks = []; let size = 0; for await (const chunk of response.body) { size += chunk.length; if (size > 64 * 1024 * 1024) throw new Error("Official tool archive exceeds 64 MiB limit."); chunks.push(chunk); } const archive = Buffer.concat(chunks); if (createHash("sha256").update(archive).digest("hex") !== tool.sha256) throw new Error(`${tool.name} archive integrity check failed.`); const archivePath = path.join(staging, "tool.zip"); await writeFile(archivePath, archive); const unpack = spawnSync("tar.exe", ["-xf", archivePath, "-C", staging, `${tool.name}.exe`], { windowsHide: true, stdio: "ignore", timeout: 30_000 }); if (unpack.status !== 0) throw new Error(`Could not unpack ${tool.name}.`); await rename(path.join(staging, `${tool.name}.exe`), target); console.log(`[PalmTTY] Verified ${tool.name} ${tool.version}.`); } finally { await rm(staging, { recursive: true, force: true }); } } } export async function exportPrivateNetworkSetup(config) { const gateway = config.server.privateGateway; if (!gateway) return; const root = privateNetworkRoot(); const publicRoot = path.join(root, "public"); await mkdir(publicRoot, { recursive: true }); const codeEnabled = config.integrations.vscodeWeb?.mode === "managed"; let certificate; if (codeEnabled) { const caPath = path.join(root, "pki", "pki", "authorities", "local", "root.crt"); certificate = new X509Certificate(await readFile(caPath)); for (const destination of [root, publicRoot]) await copyFile(caPath, path.join(destination, "PalmTTY-root.crt")); // Preserve the existing profile identity and CA; only the new IP leaf changes. const profile = ` PayloadTypeConfigurationPayloadVersion1 PayloadIdentifierhome.arpa.palmtty.root PayloadUUID1E60905C-46F7-4B16-A8B5-1542B1448A76 PayloadDisplayNamePalmTTY Private HTTPS PayloadContent PayloadTypecom.apple.security.rootPayloadVersion1 PayloadIdentifierhome.arpa.palmtty.root.certificate PayloadUUIDB35712DC-FDB0-4A66-A281-D18C6F4DC526 PayloadDisplayNamePalmTTY Private Root PayloadContent${certificate.raw.toString("base64")} `; for (const destination of [root, publicRoot]) await writeFile(path.join(destination,"PalmTTY-HTTPS.mobileconfig"),profile); } else { for (const name of ["PalmTTY-root.crt", "PalmTTY-HTTPS.mobileconfig"]) await rm(path.join(publicRoot,name),{force:true}); } const palmRoot = `http://${gateway.listenHost}:${config.server.port}/`; const setupRoot = palmRoot + "network-setup/"; const applications = Object.keys(config.integrations).filter(id=>config.integrations[id]?.mode==="managed") .map(id=>({id,url:integrationAccessUrl(config,id)})); const trustInstructions = certificate ? `Code HTTPS 首次使用需信任根证书;已有根信任无需重装。 iPhone:安装 PalmTTY-HTTPS.mobileconfig,在 设置 > 通用 > 关于本机 > 证书信任设置 启用完全信任。 Windows:certutil -user -addstore Root "${path.join(root,"PalmTTY-root.crt")}" 根证书 SHA-256:${certificate.fingerprint256}` : "当前只有 HTTP 应用,不需要证书。"; await writeFile(path.join(root,"连接说明.txt"),`PalmTTY 内网 IP 访问 入口:${palmRoot} ${applications.map(app=>app.id+":"+app.url).join("\n")} 家中 Wi-Fi 与 5G + WireGuard 使用相同地址,无需设置私有 DNS 或 hosts。 主机 IP 应固定;WireGuard AllowedIPs 需包含 ${gateway.listenHost}。 若旧 WireGuard DNS 仍为此主机,请先恢复为隧道可达的正常 DNS;本软件不再提供 DNS。 ${trustInstructions} 应用仍有各自认证。同 IP 不同端口不隔离 Cookie,仅适用于同一可信主机上的自用应用。 如需更新防火墙,请在管理员 PowerShell 明确运行: & "${path.join(root,"configure-firewall.ps1")}" 脚本只更新具名、程序/私有 IP/私有来源受限的 HTTP/HTTPS 规则,并移除旧 DNS 规则;不自动提权。 公开证书配置页:${setupRoot} 不需要公网端口转发。`,"utf8"); const certificateLinks = certificate ? `
  • Code HTTPS:iPhone 根证书描述文件;公开根证书。已信任无需重装。
  • 根证书 SHA-256:${certificate.fingerprint256}
  • ` : ""; await writeFile(path.join(publicRoot,"index.html"),`PalmTTY 内网 IP 访问

    PalmTTY 内网 IP 访问

    Wi-Fi 和 WireGuard 使用相同 IP 地址,无需私有 DNS。不要在此输入访问令牌。

    WireGuard 路由需包含主机 IP。首次根信任需明确操作,iPhone 安装后需启用完全信任。

    `,"utf8"); const caddy = path.join(root,`caddy-${PRIVATE_NETWORK_TOOLS[0].version}.exe`); const literal = value=>"'"+String(value).replaceAll("'","''")+"'"; const firewall = `# Requires an explicit administrator PowerShell. No automatic elevation. $ErrorActionPreference = 'Stop' if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this reviewed script in an administrator PowerShell.' } $privateSources = @('LocalSubnet','10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','100.64.0.0/10') foreach ($retired in @('PalmTTY-PrivateDNS-UDP','PalmTTY-PrivateDNS-TCP')) { if (Get-NetFirewallRule -Name $retired -ErrorAction SilentlyContinue) { Remove-NetFirewallRule -Name $retired } } $rules = @( @{ Name='PalmTTY-PrivateHTTPS'; Program=${literal(caddy)}; Ports=@(${privateGatewayPorts(config).join(",")}) }, @{ Name='PalmTTY-PrivateHTTP'; Program=${literal(process.execPath)}; Ports=@(${config.server.port}) } ) foreach ($rule in $rules) { if (Get-NetFirewallRule -Name $rule.Name -ErrorAction SilentlyContinue) { Remove-NetFirewallRule -Name $rule.Name } New-NetFirewallRule -Name $rule.Name -DisplayName $rule.Name -Direction Inbound -Action Allow -Profile Any -Program $rule.Program -Protocol TCP -LocalAddress ${gateway.listenHost} -LocalPort $rule.Ports -RemoteAddress $privateSources | Out-Null } Write-Output 'PalmTTY private-source HTTP/HTTPS firewall rules configured; retired DNS rules removed.' `; await writeFile(path.join(root,"configure-firewall.ps1"),firewall,"utf8"); console.log(`[PalmTTY] Private IP client setup: ${path.join(root,"连接说明.txt")}`); }