fix: 稳定 Linux Provider 公网通道

This commit is contained in:
pikastech
2026-07-20 08:01:28 +02:00
parent 4193b6b749
commit d1d551bd2e
4 changed files with 106 additions and 6 deletions
+3 -1
View File
@@ -105,10 +105,12 @@ install:
identityFile: C:\Users\liang\AppData\Local\UniDesk\hyperv-vm\D601-VM\id_ed25519
stagingDirectory: C:\Users\liang\AppData\Local\Temp\unideskcprovider-linux
providerId: D601-VM-HOST
server: ws://10.89.0.1:18082/ws/provider
user: ubuntu
home: /home/ubuntu
systemdUnit: unideskcprovider.service
network:
bypassKubernetesServiceNat: true
systemdUnit: unideskcprovider-network.service
logsTailLines: 40
credential:
sourceRef:
+77
View File
@@ -58,6 +58,16 @@ function psQuote(value: string): string {
return `'${value.replaceAll("'", "''")}'`;
}
function providerPorts(server: string): { host: string; control: number; data: number } {
const parsed = new URL(server);
const control = parsed.port.length > 0 ? Number(parsed.port) : parsed.protocol === "wss:" ? 443 : 80;
const data = control === 8081 ? 8082 : control === 18082 ? 18084 : control + 2;
if (parsed.hostname.length === 0 || !Number.isInteger(control) || !Number.isInteger(data) || data > 65535) {
throw new Error(`Linux Provider server cannot derive TCP endpoints: ${server}`);
}
return { host: parsed.hostname, control, data };
}
function checked(result: CommandResult, operation: string): CommandResult {
if (result.exitCode !== 0) {
const detail = result.stderr.trim() || result.stdout.trim() || `exitCode=${result.exitCode}`;
@@ -111,10 +121,74 @@ function plan(config: ProviderAppsConfig, id: string, target: LinuxInstallTarget
installer: `${config.publicExposure.publicBaseUrl}${config.install.routes.linuxInstaller}`,
oneLine: `curl -fsSL ${config.publicExposure.publicBaseUrl}${config.install.routes.linuxInstaller} | bash`,
service: target.systemdUnit,
network: {
bypassKubernetesServiceNat: target.network.bypassKubernetesServiceNat,
systemdUnit: target.network.systemdUnit,
endpoints: providerPorts(target.server),
},
benchmark: target.benchmark,
};
}
function installNetworkBypass(target: LinuxInstallTarget): void {
const helperPath = `/usr/local/lib/unideskcprovider/${target.providerId}-network-bypass.sh`;
if (!target.network.bypassKubernetesServiceNat) {
const script = [
"set -eu",
`sudo systemctl disable --now ${shQuote(target.network.systemdUnit)} >/dev/null 2>&1 || true`,
`sudo rm -f ${shQuote(`/etc/systemd/system/${target.network.systemdUnit}`)} ${shQuote(helperPath)}`,
"sudo systemctl daemon-reload",
].join("; ");
checked(bootstrapPowerShell(target, script), "remove Linux Provider network bypass");
return;
}
const endpoint = providerPorts(target.server);
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(endpoint.host)) {
throw new Error(`Kubernetes Service NAT bypass requires an IPv4 Provider host: ${endpoint.host}`);
}
const helper = `#!/bin/sh
set -eu
action=\${1:-start}
uid=$(id -u ${shQuote(target.user)})
for port in ${endpoint.control} ${endpoint.data}; do
if [ "$action" = start ]; then
iptables -t nat -C OUTPUT -d ${endpoint.host}/32 -p tcp --dport "$port" -m owner --uid-owner "$uid" -j RETURN 2>/dev/null || \\
iptables -t nat -I OUTPUT 1 -d ${endpoint.host}/32 -p tcp --dport "$port" -m owner --uid-owner "$uid" -j RETURN
else
while iptables -t nat -C OUTPUT -d ${endpoint.host}/32 -p tcp --dport "$port" -m owner --uid-owner "$uid" -j RETURN 2>/dev/null; do
iptables -t nat -D OUTPUT -d ${endpoint.host}/32 -p tcp --dport "$port" -m owner --uid-owner "$uid" -j RETURN
done
fi
done
`;
const unit = `[Unit]
Description=UniDesk Native Provider Kubernetes Service NAT bypass
After=network-online.target k3s-agent.service
Before=${target.systemdUnit}
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${helperPath} start
ExecStop=${helperPath} stop
[Install]
WantedBy=multi-user.target
`;
const helperBase64 = Buffer.from(helper, "utf8").toString("base64");
const unitBase64 = Buffer.from(unit, "utf8").toString("base64");
const script = [
"set -eu",
`sudo systemctl stop ${shQuote(target.network.systemdUnit)} >/dev/null 2>&1 || true`,
`printf %s ${shQuote(helperBase64)} | base64 -d | sudo install -D -m 0755 /dev/stdin ${shQuote(helperPath)}`,
`printf %s ${shQuote(unitBase64)} | base64 -d | sudo install -D -m 0644 /dev/stdin ${shQuote(`/etc/systemd/system/${target.network.systemdUnit}`)}`,
"sudo systemctl daemon-reload",
`sudo systemctl enable --now ${shQuote(target.network.systemdUnit)}`,
].join("; ");
checked(bootstrapPowerShell(target, script), "install Linux Provider network bypass");
}
function apply(config: ProviderAppsConfig, id: string, target: LinuxInstallTarget, args: string[]): Record<string, unknown> {
if (!args.includes("--confirm")) throw new Error("provider-apps linux apply requires --confirm");
const values = parseEnv(target.credential.sourceRef.path);
@@ -134,6 +208,7 @@ function apply(config: ProviderAppsConfig, id: string, target: LinuxInstallTarge
"",
].join("\n");
try {
installNetworkBypass(target);
writeFileSync(localConfig, configYaml, { encoding: "utf8", mode: 0o600 });
writeFileSync(localToken, `${token}\n`, { encoding: "utf8", mode: 0o600 });
checked(trans(target.bootstrap.route, ["ps", `New-Item -ItemType Directory -Force -Path ${psQuote(windowsStage)} | Out-Null`]), "prepare Windows SSH staging");
@@ -185,6 +260,8 @@ function status(id: string, target: LinuxInstallTarget): Record<string, unknown>
"set -eu",
`printf 'serviceActive=%s\\n' "$(systemctl is-active ${shQuote(target.systemdUnit)} 2>/dev/null || true)"`,
`printf 'serviceEnabled=%s\\n' "$(systemctl is-enabled ${shQuote(target.systemdUnit)} 2>/dev/null || true)"`,
`printf 'networkServiceActive=%s\\n' "$(systemctl is-active ${shQuote(target.network.systemdUnit)} 2>/dev/null || true)"`,
`printf 'networkServiceEnabled=%s\\n' "$(systemctl is-enabled ${shQuote(target.network.systemdUnit)} 2>/dev/null || true)"`,
`test -f ${shQuote(`${target.home}/.unidesk/provider.yaml`)} && echo configPresent=true || echo configPresent=false`,
`test -s ${shQuote(`${target.home}/.unidesk/${target.credential.targetFile}`)} && echo credentialPresent=true || echo credentialPresent=false`,
`systemctl show ${shQuote(target.systemdUnit)} -p MainPID -p NRestarts --value 2>/dev/null | sed -n '1,2p'`,
+5 -1
View File
@@ -15,8 +15,12 @@ describe("Provider Apps L1 release", () => {
expect(config.runtime.port).toBe(18121);
expect(config.publicExposure.publicBaseUrl).toBe("https://apps.hwpod.com");
expect(config.install.linux.targets["D601-VM-HOST"]?.server).toBe(
"ws://10.89.0.1:18082/ws/provider",
config.install.defaults.server,
);
expect(config.install.linux.targets["D601-VM-HOST"]?.network).toEqual({
bypassKubernetesServiceNat: true,
systemdUnit: "unideskcprovider-network.service",
});
});
test("serves release files and rejects unknown or mutating requests", async () => {
+21 -4
View File
@@ -73,6 +73,10 @@ export interface LinuxInstallTarget {
user: string;
home: string;
systemdUnit: string;
network: {
bypassKubernetesServiceNat: boolean;
systemdUnit: string;
};
logsTailLines: number;
credential: {
sourceRef: { path: string; key: string };
@@ -190,7 +194,7 @@ function pythonRuntime(value: unknown, path: string): PythonRuntime {
return result;
}
function linuxInstall(value: unknown, path: string): LinuxInstallConfig {
function linuxInstall(value: unknown, path: string, defaultServer: string): LinuxInstallConfig {
const raw = record(value, path);
const pipSources = raw.pipSources;
if (!Array.isArray(pipSources) || pipSources.length === 0) throw new Error(`${path}.pipSources must be a non-empty list`);
@@ -206,6 +210,14 @@ function linuxInstall(value: unknown, path: string): LinuxInstallConfig {
if (providerId !== targetId) throw new Error(`${path}.targets.${targetId}.providerId must match its target key`);
const systemdUnit = string(target.systemdUnit, `${path}.targets.${targetId}.systemdUnit`);
if (!/^[A-Za-z0-9_.@-]+\.service$/u.test(systemdUnit)) throw new Error(`${path}.targets.${targetId}.systemdUnit is invalid`);
const network = record(target.network, `${path}.targets.${targetId}.network`);
if (typeof network.bypassKubernetesServiceNat !== "boolean") {
throw new Error(`${path}.targets.${targetId}.network.bypassKubernetesServiceNat must be boolean`);
}
const networkSystemdUnit = string(network.systemdUnit, `${path}.targets.${targetId}.network.systemdUnit`);
if (!/^[A-Za-z0-9_.@-]+\.service$/u.test(networkSystemdUnit)) {
throw new Error(`${path}.targets.${targetId}.network.systemdUnit is invalid`);
}
const bootstrap = record(target.bootstrap, `${path}.targets.${targetId}.bootstrap`);
const bootstrapMode = choice(bootstrap.mode, `${path}.targets.${targetId}.bootstrap.mode`, ["windowsSsh"] as const);
const guest = string(bootstrap.guest, `${path}.targets.${targetId}.bootstrap.guest`);
@@ -219,10 +231,14 @@ function linuxInstall(value: unknown, path: string): LinuxInstallConfig {
stagingDirectory: string(bootstrap.stagingDirectory, `${path}.targets.${targetId}.bootstrap.stagingDirectory`),
},
providerId,
server: url(target.server, `${path}.targets.${targetId}.server`, ["ws:", "wss:"]),
server: defaultServer,
user: string(target.user, `${path}.targets.${targetId}.user`),
home: absolutePosixPath(target.home, `${path}.targets.${targetId}.home`),
systemdUnit,
network: {
bypassKubernetesServiceNat: network.bypassKubernetesServiceNat,
systemdUnit: networkSystemdUnit,
},
logsTailLines: integer(target.logsTailLines, `${path}.targets.${targetId}.logsTailLines`),
credential: {
sourceRef: {
@@ -291,6 +307,7 @@ export function readProviderAppsConfig(repositoryRoot: string): ProviderAppsConf
const installRoutes = record(install.routes, "install.routes");
const installDefaults = record(install.defaults, "install.defaults");
const installRuntime = record(install.runtime, "install.runtime");
const defaultServer = url(installDefaults.server, "install.defaults.server", ["ws:", "wss:"]);
if (typeof installDefaults.autoStart !== "boolean") throw new Error("install.defaults.autoStart must be boolean");
const result: ProviderAppsConfig = {
configPath,
@@ -342,7 +359,7 @@ export function readProviderAppsConfig(repositoryRoot: string): ProviderAppsConf
panelImage: httpPath(installRoutes.panelImage, "install.routes.panelImage"),
},
defaults: {
server: string(installDefaults.server, "install.defaults.server"),
server: defaultServer,
credentialFile: string(installDefaults.credentialFile, "install.defaults.credentialFile"),
autoStart: installDefaults.autoStart,
pythonRuntime: choice(
@@ -364,7 +381,7 @@ export function readProviderAppsConfig(repositoryRoot: string): ProviderAppsConf
windows7: pythonRuntime(installRuntime.windows7, "install.runtime.windows7"),
windowsModern: pythonRuntime(installRuntime.windowsModern, "install.runtime.windowsModern"),
},
linux: linuxInstall(install.linux, "install.linux"),
linux: linuxInstall(install.linux, "install.linux", defaultServer),
},
publicExposure: publicExposure(root.publicExposure, "publicExposure"),
};