fix: 稳定 Linux Provider 公网通道
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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'`,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user