chore(platform-infra): commit parallel proxy updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# UniDesk Agent Index
|
||||
|
||||
UniDesk 是一个以主 server 为统一入口的分布式工作平台。本文件只做自动加载的顶级索引;长期规则、详细流程和判定标准必须放到 skill 或 `docs/reference/*.md`。
|
||||
UniDesk 以主 server 为统一入口。本文件只做自动加载的顶级索引;长期规则、流程和判定标准必须放到 skill 或 `docs/reference/*.md`。
|
||||
|
||||
## P0: 文件体积与脚本分流
|
||||
|
||||
@@ -11,9 +11,9 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台。本文
|
||||
|
||||
- P0: 发现固定主/目标 worktree 落后 remote 时,必须立刻先 `git stash push -u` 保存脏改(如有,含 untracked),再 `git pull --ff-only` 快进到最新 remote,然后 `git stash apply` 并按语义合并;主工作区恢复出的并行改动必须先直接提交,再继续后续任务;禁止用 reset、drop 或覆盖式 checkout 丢弃并行改动。
|
||||
|
||||
## P0: backend-core 运行面冻结
|
||||
## P0: NC01 k8s server 固定运行面
|
||||
|
||||
- P0: backend-core 运行面恢复 healthy 后,除非用户明确要求,不得再触碰 backend-core 运行面;禁止主动执行 `server rebuild backend-core`、`server restart backend-core`、手工重建/重启/替换 `unidesk-backend-core` 容器或等价生产变更。
|
||||
- P0: NC01 用 k8s 部署 UniDesk server/backend-core 是固定原则;恢复、验证和 `trans` 必须以 `config/unidesk-host-k8s.yaml` 与 k8s `unidesk` namespace 为准,禁止把 Docker Compose/`unidesk-backend-core` 容器当 NC01 运行面;healthy 后非明确要求不得重建/重启/替换。
|
||||
|
||||
## P0: AGENTS.md 体积与分流
|
||||
|
||||
|
||||
@@ -26,6 +26,23 @@ server:
|
||||
host: 127.0.0.1
|
||||
port: 18792
|
||||
sources:
|
||||
pk01-nc01-vpn-server-hysteria:
|
||||
sourceType: vpn-server-hysteria-existing
|
||||
serverRef: /root/vpn-server
|
||||
benchmarkRef: local-vpn-server
|
||||
implementationRef: /root/vpn-server
|
||||
sourceConfigRef: /root/vpn-server/output/client.yaml
|
||||
expectedServer: 152-53-229-148.nip.io:443
|
||||
client:
|
||||
mode: external-existing-hysteria
|
||||
configPath: /etc/host-proxy/hysteria-client.yaml
|
||||
serviceName: host-proxy-hysteria.service
|
||||
httpListenHost: 127.0.0.1
|
||||
httpListenPort: 10809
|
||||
socks5ListenHost: 127.0.0.1
|
||||
socks5ListenPort: 10808
|
||||
proxyUrl: http://127.0.0.1:10809
|
||||
externalProbeUrl: https://www.gstatic.com/generate_204
|
||||
nc01-vpn-server-shadowsocks:
|
||||
extends: templates.sources.singBoxHostClient
|
||||
benchmarkRef: local-vpn-server
|
||||
@@ -33,6 +50,24 @@ sources:
|
||||
extends: templates.sources.singBoxHostClient
|
||||
benchmarkRef: "pikasTech/unidesk#1110"
|
||||
targets:
|
||||
PK01:
|
||||
extends: templates.targets.hostProxyClient
|
||||
route: PK01
|
||||
sourceRef: sources.pk01-nc01-vpn-server-hysteria
|
||||
env:
|
||||
httpProxy: http://127.0.0.1:10809
|
||||
httpsProxy: http://127.0.0.1:10809
|
||||
allProxy: http://127.0.0.1:10809
|
||||
noProxyRefs:
|
||||
common: noProxy.common
|
||||
extras: noProxy.nodeExtras.PK01
|
||||
trans:
|
||||
hostProxyEnv:
|
||||
enabled: false
|
||||
apply:
|
||||
reloadSystemd: false
|
||||
restartDocker: false
|
||||
restartK3s: false
|
||||
NC01:
|
||||
extends: templates.targets.hostProxyClient
|
||||
route: NC01
|
||||
@@ -133,8 +168,11 @@ noProxy:
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
nodeExtras:
|
||||
PK01:
|
||||
- 152.53.229.148
|
||||
- 152-53-229-148.nip.io
|
||||
NC01:
|
||||
- 152.53.229.148
|
||||
JD01:
|
||||
- 74.48.78.17
|
||||
- 82.156.23.220
|
||||
- 82.156.23.220
|
||||
|
||||
@@ -94,15 +94,16 @@ manualAccounts:
|
||||
kind: proxy
|
||||
provider: target-egress-proxy
|
||||
description: Bind a protected manual account to the selected target's YAML-declared egress proxy.
|
||||
pk01-local-egress-proxy:
|
||||
pk01-host-proxy:
|
||||
enabled: true
|
||||
kind: proxy
|
||||
provider: fixed-http-proxy
|
||||
description: Bind a protected manual account to PK01 local provider-gateway egress.
|
||||
description: Bind protected/manual PK01 Sub2API accounts to the PK01 host proxy HTTP endpoint.
|
||||
hostProxyRef: config/platform-infra/host-proxy.yaml#targets.PK01
|
||||
fixedProxy:
|
||||
protocol: http
|
||||
host: 127.0.0.1
|
||||
port: 18789
|
||||
port: 10809
|
||||
unified-pool-group:
|
||||
enabled: true
|
||||
kind: group
|
||||
@@ -113,7 +114,7 @@ manualAccounts:
|
||||
reason: Manually configured in Sub2API as protected; keep outside pool auto-management and sentinel control.
|
||||
proxyBinding:
|
||||
enabled: true
|
||||
source: pk01-local-egress-proxy
|
||||
source: pk01-host-proxy
|
||||
proxyName: platform-infra-sub2api-pk01-local-egress-proxy
|
||||
groupBinding:
|
||||
enabled: true
|
||||
|
||||
+33
-2
@@ -14,6 +14,33 @@ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
SH
|
||||
```
|
||||
|
||||
When the PK01 provider-gateway channel is offline, operators may use the ignored break-glass SSH source `.env/PK01_ssh.txt`. The file format is two lines: line 1 is the SSH target such as `ubuntu@<host>`, line 2 is the password. Do not print the second line, commit it, copy it into issue comments, or pass it through commands that echo typed input. Prefer an ephemeral `SSH_ASKPASS` wrapper so OpenSSH can handle `keyboard-interactive` or password prompts without exposing the value:
|
||||
|
||||
```bash
|
||||
tmp=$(mktemp -d /tmp/pk01-askpass.XXXXXX)
|
||||
chmod 700 "$tmp"
|
||||
cat >"$tmp/askpass.sh" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '%s\n' "$PK01_SSH_PASS"
|
||||
SH
|
||||
chmod 700 "$tmp/askpass.sh"
|
||||
|
||||
PK01_SSH_PASS="$(sed -n '2p' .env/PK01_ssh.txt)" \
|
||||
SSH_ASKPASS="$tmp/askpass.sh" \
|
||||
SSH_ASKPASS_REQUIRE=force \
|
||||
DISPLAY=none \
|
||||
setsid -w ssh \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=8 \
|
||||
-o PreferredAuthentications=keyboard-interactive,password,publickey \
|
||||
"$(sed -n '1p' .env/PK01_ssh.txt)" \
|
||||
'hostname; date -Is'
|
||||
|
||||
rm -rf "$tmp"
|
||||
```
|
||||
|
||||
This fallback is for bounded host diagnostics or node-local recovery only. After restoring `unidesk-provider-gateway-pk01`, return to `trans PK01 ...` and `platform-infra ... --target PK01` as the operating truth.
|
||||
|
||||
Before closing an operation, verify both the provider channel and host workload state:
|
||||
|
||||
```bash
|
||||
@@ -36,7 +63,9 @@ PK01 currently uses a direct Docker provider-gateway deployment rather than a fu
|
||||
| Env file | `/home/ubuntu/.unidesk/state/provider-pk01/provider.env` | Contains provider token and must not be printed, copied into docs, or committed. |
|
||||
| Host SSH key | `/home/ubuntu/.unidesk/host-ssh-pk01/id_ed25519` | Mounted read-only at `/run/host-ssh`; public key is authorized for `ubuntu`. |
|
||||
| Logs | `/home/ubuntu/.unidesk/logs/provider-pk01` | Node-local runtime logs, not a Git source of truth. |
|
||||
| Egress proxy | `127.0.0.1:18789` | Loopback only; never expose as a public endpoint. |
|
||||
| Host SSH target | `root@host.docker.internal:22` | `trans PK01 ...` is expected to land as root for host/systemd operations. |
|
||||
| Provider-gateway egress proxy | `127.0.0.1:18789` | Provider maintenance tunnel only; do not bind Sub2API upstream accounts to this endpoint. |
|
||||
| Host proxy HTTP endpoint | `127.0.0.1:10809` | PK01 host proxy endpoint for Sub2API account proxy binding, declared by `config/platform-infra/host-proxy.yaml#targets.PK01`. |
|
||||
|
||||
Long-term provider-gateway upgrades should converge to the standard `provider.upgrade mode=schedule` flow described in `docs/reference/provider-gateway.md`. If PK01 is still on the direct Docker bootstrap path, do not rebuild the gateway synchronously through the gateway's own `trans PK01` session. Use a detached node-local job or first move PK01 to the standard attach/upgrade bundle.
|
||||
|
||||
@@ -68,7 +97,9 @@ If one public service fails while other services still work, restore the missing
|
||||
|
||||
In this mode, public ports `80` and `443` belong to Caddy. The existing `pikanode` container must be bound to a loopback HTTP port and used only as the apex PikaPython/PikaNode upstream. Public platform routes must be read from their owning YAML. For the current Sub2API PK01 host-Docker target, `api.pikapython.com` uses `publicExposure.mode=pk01-local` and API traffic follows `client -> PK01 Caddy -> 127.0.0.1:<YAML local upstream port> -> PK01 host-Docker Sub2API`. For k3s external-active targets, the route may instead be `client -> PK01 Caddy -> PK01 frps remote port -> target frpc -> target Sub2API`. Neither path may pass through pikanode or a master-server reverse proxy.
|
||||
|
||||
Caddy binary installation is also YAML-controlled. If `publicExposure.pk01.caddyDownloadProxyUrl` is set, PK01 Caddy downloads must use that proxy URL; the PK01 loopback provider egress proxy is the preferred source. A slow or failing Caddy download should first be treated as missing proxy use, not as a reason to keep retrying a naked GitHub release download.
|
||||
Caddy binary installation is also YAML-controlled. If `publicExposure.pk01.caddyDownloadProxyUrl` is set, PK01 Caddy downloads must use that proxy URL. Sub2API account-level proxy binding must use the PK01 host proxy HTTP endpoint, not the provider-gateway egress proxy.
|
||||
|
||||
PK01's account-level host proxy is a status-only Hysteria client connected to the NC01-local `../vpn-server` deployment. The durable declaration is `config/platform-infra/host-proxy.yaml#targets.PK01`, and protected/manual Sub2API account bindings should reference it through `config/platform-infra/sub2api-codex-pool.yaml` `manualAccounts.bindingSources.*.hostProxyRef`. Do not replace it with `127.0.0.1:18789`; verify it with `bun scripts/cli.ts platform-infra egress-proxy host status --target PK01` and then validate the Sub2API account/user request path.
|
||||
|
||||
The public certificate depends on DNS. The `api.pikapython.com` record must resolve to the YAML-declared PK01 public address before Caddy can complete ACME issuance. If the DNS record is absent or stale, local probes such as PK01 loopback app health checks, loopback Caddy `--resolve` checks, or target-specific FRP remote-port checks can prove parts of the data path, but final `https://api.pikapython.com` validation remains blocked until DNS is corrected.
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ interface HostProxyServer {
|
||||
|
||||
interface HostProxySource {
|
||||
id: string;
|
||||
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks";
|
||||
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks" | "vpn-server-hysteria-existing";
|
||||
serverRef: string;
|
||||
benchmarkRef: string;
|
||||
implementationRef: string;
|
||||
@@ -51,14 +51,17 @@ interface HostProxySource {
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
sourceFingerprint: string;
|
||||
masterShadowsocks: MasterShadowsocksSourceSpec;
|
||||
masterShadowsocks: MasterShadowsocksSourceSpec | null;
|
||||
client: HostProxyClient;
|
||||
proxyUrl: string;
|
||||
externalProbeUrl: string;
|
||||
expectedServer: string | null;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
interface HostProxyClient {
|
||||
type HostProxyClient = HostProxyManagedClient | HostProxyExistingHysteriaClient;
|
||||
|
||||
interface HostProxyManagedClient {
|
||||
mode: "trans-static-binary";
|
||||
upstreamUrl: string;
|
||||
version: string;
|
||||
@@ -79,6 +82,16 @@ interface HostProxyClient {
|
||||
healthUrl: string;
|
||||
}
|
||||
|
||||
interface HostProxyExistingHysteriaClient {
|
||||
mode: "external-existing-hysteria";
|
||||
configPath: string;
|
||||
serviceName: string;
|
||||
httpListenHost: string;
|
||||
httpListenPort: number;
|
||||
socks5ListenHost: string;
|
||||
socks5ListenPort: number;
|
||||
}
|
||||
|
||||
interface HostProxyPodAccess {
|
||||
enabled: boolean;
|
||||
listenHost: string;
|
||||
@@ -136,14 +149,11 @@ export async function runPlatformInfraHostProxyCommand(_config: UniDeskConfig, a
|
||||
if (options.action === "plan") return renderPlan(plan(config.server, target));
|
||||
if (options.action === "status") return renderStatus(status(config.server, target));
|
||||
if (!options.confirm || options.dryRun) {
|
||||
const dryRunPlan = plan(config.server, target);
|
||||
return renderPlan({
|
||||
...plan(config.server, target),
|
||||
...dryRunPlan,
|
||||
mode: "dry-run",
|
||||
mutation: false,
|
||||
next: {
|
||||
apply: `bun scripts/cli.ts platform-infra egress-proxy host apply --target ${target.id} --confirm`,
|
||||
status: `bun scripts/cli.ts platform-infra egress-proxy host status --target ${target.id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return renderApply(apply(config.server, target));
|
||||
@@ -233,13 +243,14 @@ function parseServer(raw: Record<string, unknown>, label: string): HostProxyServ
|
||||
}
|
||||
|
||||
function parseSource(id: string, raw: Record<string, unknown>): HostProxySource {
|
||||
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks"] as const);
|
||||
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks", "vpn-server-hysteria-existing"] as const);
|
||||
if (sourceType === "vpn-server-hysteria-existing") return parseExistingHysteriaSource(id, raw, sourceType);
|
||||
const sourceConfigRef = stringField(raw, "sourceConfigRef", `${configPath}.sources.${id}`);
|
||||
const resolved = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.sources.${id}.sourceConfigRef`);
|
||||
if (resolved.sourceType !== "master-shadowsocks" || resolved.masterShadowsocks === null) {
|
||||
throw new Error(`${configPath}.sources.${id}.sourceConfigRef must reference a master-shadowsocks source`);
|
||||
}
|
||||
const client = clientSpec(record(raw.client, `${configPath}.sources.${id}.client`), `${configPath}.sources.${id}.client`);
|
||||
const client = managedClientSpec(record(raw.client, `${configPath}.sources.${id}.client`), `${configPath}.sources.${id}.client`);
|
||||
const proxyUrl = urlField(raw, "proxyUrl", `${configPath}.sources.${id}`);
|
||||
const expectedProxyUrl = `http://${client.listenHost}:${client.listenPort}`;
|
||||
if (proxyUrl !== expectedProxyUrl) throw new Error(`${configPath}.sources.${id}.proxyUrl must be ${expectedProxyUrl}`);
|
||||
@@ -257,6 +268,7 @@ function parseSource(id: string, raw: Record<string, unknown>): HostProxySource
|
||||
client,
|
||||
proxyUrl,
|
||||
externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`),
|
||||
expectedServer: null,
|
||||
fingerprint: "",
|
||||
};
|
||||
return {
|
||||
@@ -274,7 +286,46 @@ function parseSource(id: string, raw: Record<string, unknown>): HostProxySource
|
||||
};
|
||||
}
|
||||
|
||||
function clientSpec(raw: Record<string, unknown>, label: string): HostProxyClient {
|
||||
function parseExistingHysteriaSource(id: string, raw: Record<string, unknown>, sourceType: HostProxySource["sourceType"]): HostProxySource {
|
||||
const client = existingHysteriaClientSpec(record(raw.client, `${configPath}.sources.${id}.client`), `${configPath}.sources.${id}.client`);
|
||||
const proxyUrl = urlField(raw, "proxyUrl", `${configPath}.sources.${id}`);
|
||||
const expectedProxyUrl = `http://${client.httpListenHost}:${client.httpListenPort}`;
|
||||
if (proxyUrl !== expectedProxyUrl) throw new Error(`${configPath}.sources.${id}.proxyUrl must be ${expectedProxyUrl}`);
|
||||
const sourceConfigRef = stringField(raw, "sourceConfigRef", `${configPath}.sources.${id}`);
|
||||
const expectedServer = listenAddressField(raw, "expectedServer", `${configPath}.sources.${id}`);
|
||||
const source = {
|
||||
id,
|
||||
sourceType,
|
||||
serverRef: stringField(raw, "serverRef", `${configPath}.sources.${id}`),
|
||||
benchmarkRef: stringField(raw, "benchmarkRef", `${configPath}.sources.${id}`),
|
||||
implementationRef: stringField(raw, "implementationRef", `${configPath}.sources.${id}`),
|
||||
sourceConfigRef,
|
||||
sourceRef: sourceConfigRef,
|
||||
sourceKey: "",
|
||||
sourceFingerprint: fingerprint({ sourceConfigRef, expectedServer }),
|
||||
masterShadowsocks: null,
|
||||
client,
|
||||
proxyUrl,
|
||||
externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`),
|
||||
expectedServer,
|
||||
fingerprint: "",
|
||||
};
|
||||
return {
|
||||
...source,
|
||||
fingerprint: fingerprint({
|
||||
sourceType,
|
||||
benchmarkRef: source.benchmarkRef,
|
||||
implementationRef: source.implementationRef,
|
||||
sourceConfigRef,
|
||||
expectedServer,
|
||||
client,
|
||||
proxyUrl,
|
||||
externalProbeUrl: source.externalProbeUrl,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function managedClientSpec(raw: Record<string, unknown>, label: string): HostProxyManagedClient {
|
||||
const mode = enumField(raw, "mode", label, ["trans-static-binary"] as const);
|
||||
const podAccess = raw.podAccess === undefined ? null : podAccessSpec(record(raw.podAccess, `${label}.podAccess`), `${label}.podAccess`);
|
||||
const client = {
|
||||
@@ -302,6 +353,22 @@ function clientSpec(raw: Record<string, unknown>, label: string): HostProxyClien
|
||||
return client;
|
||||
}
|
||||
|
||||
function existingHysteriaClientSpec(raw: Record<string, unknown>, label: string): HostProxyExistingHysteriaClient {
|
||||
const mode = enumField(raw, "mode", label, ["external-existing-hysteria"] as const);
|
||||
const client = {
|
||||
mode,
|
||||
configPath: absolutePathField(raw, "configPath", label),
|
||||
serviceName: stringField(raw, "serviceName", label),
|
||||
httpListenHost: hostField(raw, "httpListenHost", label),
|
||||
httpListenPort: portField(raw, "httpListenPort", label),
|
||||
socks5ListenHost: hostField(raw, "socks5ListenHost", label),
|
||||
socks5ListenPort: portField(raw, "socks5ListenPort", label),
|
||||
};
|
||||
if (client.httpListenHost !== "127.0.0.1") throw new Error(`${label}.httpListenHost must stay 127.0.0.1 for host-local HTTP proxy`);
|
||||
if (client.socks5ListenHost !== "127.0.0.1") throw new Error(`${label}.socks5ListenHost must stay 127.0.0.1 for host-local SOCKS5 proxy`);
|
||||
return client;
|
||||
}
|
||||
|
||||
function podAccessSpec(raw: Record<string, unknown>, label: string): HostProxyPodAccess {
|
||||
const enabled = booleanField(raw, "enabled", label);
|
||||
const listenHost = hostField(raw, "listenHost", label);
|
||||
@@ -394,6 +461,7 @@ function resolveTarget(config: HostProxyConfig, targetId: string): HostProxyTarg
|
||||
|
||||
function plan(server: HostProxyServer, target: HostProxyTarget): Record<string, unknown> {
|
||||
const client = target.source.client;
|
||||
const applyEnabled = client.mode === "trans-static-binary";
|
||||
return {
|
||||
ok: true,
|
||||
command: "platform-infra egress-proxy host plan",
|
||||
@@ -407,14 +475,17 @@ function plan(server: HostProxyServer, target: HostProxyTarget): Record<string,
|
||||
env: { proxyUrl: target.env.httpProxy, noProxyCount: target.env.noProxy.length, noProxyIncludesHyueapi: true },
|
||||
valuesPrinted: false,
|
||||
next: {
|
||||
dryRun: `bun scripts/cli.ts platform-infra egress-proxy host apply --target ${target.id} --dry-run`,
|
||||
apply: `bun scripts/cli.ts platform-infra egress-proxy host apply --target ${target.id} --confirm`,
|
||||
dryRun: applyEnabled ? `bun scripts/cli.ts platform-infra egress-proxy host apply --target ${target.id} --dry-run` : "disabled: existing external host proxy client",
|
||||
apply: applyEnabled ? `bun scripts/cli.ts platform-infra egress-proxy host apply --target ${target.id} --confirm` : "disabled: existing external host proxy client",
|
||||
status: `bun scripts/cli.ts platform-infra egress-proxy host status --target ${target.id}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function apply(server: HostProxyServer, target: HostProxyTarget): Record<string, unknown> {
|
||||
if (target.source.client.mode !== "trans-static-binary") {
|
||||
throw new Error(`${configPath}.targets.${target.id} uses ${target.source.client.mode}; host apply is disabled for existing external clients. Use status to verify the declared runtime.`);
|
||||
}
|
||||
const material = proxySecretMaterial(server, target.source);
|
||||
const serverApply = sourceUsesManagedServer(target.source) ? applyMasterProxyServer(server, material) : skippedMasterProxyServer(target.source, "apply");
|
||||
const artifact = prepareClientArtifact(target.source.client);
|
||||
@@ -495,13 +566,14 @@ function skippedMasterProxyServer(source: HostProxySource, mode: "apply" | "stat
|
||||
existingHealthy: null,
|
||||
inspect: null,
|
||||
container: "external",
|
||||
listen: `${source.masterShadowsocks.serverHost}:${source.masterShadowsocks.serverPort}`,
|
||||
listen: source.masterShadowsocks === null ? source.expectedServer ?? source.proxyUrl : `${source.masterShadowsocks.serverHost}:${source.masterShadowsocks.serverPort}`,
|
||||
health: { ok: true, mode: "external", host: "", port: 0 },
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function proxySecretMaterial(server: HostProxyServer, source: HostProxySource): HostProxySecretMaterial {
|
||||
if (source.masterShadowsocks === null) throw new Error(`${configPath}.sources.${source.id} does not render managed shadowsocks secret material`);
|
||||
const envSource = readEnvSourceFile({
|
||||
root: rootPath(".state", "secrets"),
|
||||
sourceRef: source.sourceRef,
|
||||
@@ -531,6 +603,9 @@ function renderMasterShadowsocksServerConfig(server: HostProxyServer, password:
|
||||
}
|
||||
|
||||
function renderSingBoxClientConfig(source: HostProxySource, password: string): string {
|
||||
if (source.masterShadowsocks === null || source.client.mode !== "trans-static-binary") {
|
||||
throw new Error(`${configPath}.sources.${source.id} cannot render sing-box client config`);
|
||||
}
|
||||
const client = source.client;
|
||||
const inbounds = [
|
||||
{ type: "mixed", tag: "mixed-host-local", listen: client.listenHost, listen_port: client.listenPort },
|
||||
@@ -741,6 +816,7 @@ ${remoteStatusProbeShell(target)}
|
||||
|
||||
function remoteStatusProbeShell(target: HostProxyTarget): string {
|
||||
const client = target.source.client;
|
||||
if (client.mode === "external-existing-hysteria") return remoteExistingHysteriaStatusProbeShell(target, client);
|
||||
const podAccess = client.podAccess;
|
||||
const podAccessShell = podAccess?.enabled
|
||||
? `
|
||||
@@ -811,6 +887,101 @@ PY
|
||||
`;
|
||||
}
|
||||
|
||||
function remoteExistingHysteriaStatusProbeShell(target: HostProxyTarget, client: HostProxyExistingHysteriaClient): string {
|
||||
return `
|
||||
set -eu
|
||||
config_present=missing
|
||||
[ -f ${shQuote(client.configPath)} ] && config_present=present
|
||||
service_active="$(systemctl is-active ${shQuote(client.serviceName)} 2>/dev/null || true)"
|
||||
http_rc=0
|
||||
http_metrics="$(curl -sS -o /dev/null -w '%{http_code} %{time_connect} %{time_pretransfer} %{time_starttransfer} %{time_total}' --max-time 20 -x ${shQuote(target.env.httpProxy)} ${shQuote(target.source.externalProbeUrl)} 2>/tmp/unidesk-host-proxy-hysteria-http.err)" || http_rc=$?
|
||||
socks_rc=0
|
||||
socks_metrics="$(curl -sS -o /dev/null -w '%{http_code} %{time_connect} %{time_pretransfer} %{time_starttransfer} %{time_total}' --max-time 20 --socks5-hostname ${shQuote(`${client.socks5ListenHost}:${client.socks5ListenPort}`)} ${shQuote(target.source.externalProbeUrl)} 2>/tmp/unidesk-host-proxy-hysteria-socks.err)" || socks_rc=$?
|
||||
python3 - "$config_present" "$service_active" "$http_rc" "$http_metrics" "$socks_rc" "$socks_metrics" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
config_path = "${client.configPath}"
|
||||
expected_server = "${target.source.expectedServer ?? ""}"
|
||||
expected_http = "${client.httpListenHost}:${client.httpListenPort}"
|
||||
expected_socks = "${client.socks5ListenHost}:${client.socks5ListenPort}"
|
||||
|
||||
def parse_metrics(raw):
|
||||
parts = str(raw or "").split()
|
||||
if len(parts) != 5:
|
||||
return {"status": "", "timeConnect": "", "timePretransfer": "", "timeStarttransfer": "", "timeTotal": ""}
|
||||
return {
|
||||
"status": parts[0],
|
||||
"timeConnect": parts[1],
|
||||
"timePretransfer": parts[2],
|
||||
"timeStarttransfer": parts[3],
|
||||
"timeTotal": parts[4],
|
||||
}
|
||||
|
||||
def section_listen(text, section):
|
||||
match = re.search(r"(?ms)^" + re.escape(section) + r":\\s*\\n(?:[ \\t]+.*\\n)*?[ \\t]+listen:\\s*([^\\s#]+)", text)
|
||||
return match.group(1).strip() if match else ""
|
||||
|
||||
text = Path(config_path).read_text(encoding="utf-8") if Path(config_path).exists() else ""
|
||||
server_match = re.search(r"(?m)^server:\\s*([^\\s#]+)", text)
|
||||
server = server_match.group(1).strip() if server_match else ""
|
||||
server_host_port = re.sub(r"^[^@]*@", "", server)
|
||||
http_listen = section_listen(text, "http")
|
||||
socks_listen = section_listen(text, "socks5")
|
||||
http = parse_metrics(sys.argv[4])
|
||||
socks = parse_metrics(sys.argv[6])
|
||||
payload = {
|
||||
"ok": (
|
||||
sys.argv[1] == "present"
|
||||
and sys.argv[2] == "active"
|
||||
and server_host_port == expected_server
|
||||
and http_listen == expected_http
|
||||
and socks_listen == expected_socks
|
||||
and int(sys.argv[3]) == 0
|
||||
and http["status"] == "204"
|
||||
and int(sys.argv[5]) == 0
|
||||
and socks["status"] == "204"
|
||||
),
|
||||
"target": "${target.id}",
|
||||
"route": "${target.route}",
|
||||
"sourceRef": "${target.sourceRef}",
|
||||
"sourceFingerprint": "${target.source.fingerprint}",
|
||||
"client": {
|
||||
"mode": "${client.mode}",
|
||||
"service": "${client.serviceName}",
|
||||
"serviceActive": sys.argv[2],
|
||||
"configPath": config_path,
|
||||
"configPresent": sys.argv[1],
|
||||
"configMatches": server_host_port == expected_server and http_listen == expected_http and socks_listen == expected_socks,
|
||||
"server": server_host_port,
|
||||
"expectedServer": expected_server,
|
||||
"httpListen": http_listen,
|
||||
"socks5Listen": socks_listen,
|
||||
"listen": expected_http,
|
||||
"podAccess": {"enabled": False, "listen": "", "proxyUrl": ""},
|
||||
},
|
||||
"files": {
|
||||
"envFile": "status-only",
|
||||
"profile": "status-only",
|
||||
"apt": "status-only",
|
||||
"dockerSystemdDropIn": "status-only",
|
||||
"k3sSystemdDropIn": "status-only",
|
||||
},
|
||||
"noProxy": {"hyueapi": True, "wildcardHyueapi": True},
|
||||
"health": {"exitCode": int(sys.argv[3]), "url": "${target.source.externalProbeUrl}", "mode": "http-proxy"},
|
||||
"externalProbe": {"exitCode": int(sys.argv[3]), **http, "url": "${target.source.externalProbeUrl}", "proxyUrl": "${target.env.httpProxy}"},
|
||||
"socks5Probe": {"exitCode": int(sys.argv[5]), **socks, "url": "${target.source.externalProbeUrl}", "proxyUrl": "socks5h://${client.socks5ListenHost}:${client.socks5ListenPort}"},
|
||||
"podAccessProbe": {"enabled": False, "exitCode": 0, "status": "skipped", "url": "${target.source.externalProbeUrl}"},
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
}
|
||||
|
||||
function renderClientUnit(client: HostProxyClient): string {
|
||||
return [
|
||||
"[Unit]",
|
||||
@@ -900,15 +1071,7 @@ function targetSummary(target: HostProxyTarget): Record<string, unknown> {
|
||||
proxyUrl: target.source.proxyUrl,
|
||||
benchmarkRef: target.source.benchmarkRef,
|
||||
implementationRef: target.source.implementationRef,
|
||||
client: {
|
||||
mode: client.mode,
|
||||
version: client.version,
|
||||
installPath: client.installPath,
|
||||
configPath: client.configPath,
|
||||
unitPath: client.unitPath,
|
||||
serviceName: client.serviceName,
|
||||
listen: `${client.listenHost}:${client.listenPort}`,
|
||||
},
|
||||
client: clientSummary(client),
|
||||
trans: {
|
||||
hostProxyEnv: transHostProxyEnvSummary(target.id),
|
||||
},
|
||||
@@ -916,6 +1079,19 @@ function targetSummary(target: HostProxyTarget): Record<string, unknown> {
|
||||
}
|
||||
|
||||
function artifactSummary(client: HostProxyClient): Record<string, unknown> {
|
||||
if (client.mode === "external-existing-hysteria") {
|
||||
return {
|
||||
mode: client.mode,
|
||||
version: "existing",
|
||||
upstreamUrl: "external",
|
||||
archiveCachePath: "external",
|
||||
binaryCachePath: "external",
|
||||
archiveSha256: "external",
|
||||
archiveInstallPath: "external",
|
||||
binarySha256: "external",
|
||||
installPath: client.configPath,
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: client.mode,
|
||||
version: client.version,
|
||||
@@ -929,6 +1105,30 @@ function artifactSummary(client: HostProxyClient): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function clientSummary(client: HostProxyClient): Record<string, unknown> {
|
||||
if (client.mode === "external-existing-hysteria") {
|
||||
return {
|
||||
mode: client.mode,
|
||||
version: "existing",
|
||||
installPath: "external",
|
||||
configPath: client.configPath,
|
||||
unitPath: "external",
|
||||
serviceName: client.serviceName,
|
||||
listen: `${client.httpListenHost}:${client.httpListenPort}`,
|
||||
socks5Listen: `${client.socks5ListenHost}:${client.socks5ListenPort}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: client.mode,
|
||||
version: client.version,
|
||||
installPath: client.installPath,
|
||||
configPath: client.configPath,
|
||||
unitPath: client.unitPath,
|
||||
serviceName: client.serviceName,
|
||||
listen: `${client.listenHost}:${client.listenPort}`,
|
||||
};
|
||||
}
|
||||
|
||||
function masterProxyTcpHealth(server: HostProxyServer): Record<string, unknown> {
|
||||
const result = runCommand(["bash", "-lc", `timeout 5 bash -lc ${shQuote(`</dev/tcp/${server.health.host}/${server.health.port}`)}`], rootPath(), { timeoutMs: 8_000 });
|
||||
return { ok: result.exitCode === 0, mode: server.health.mode, host: server.health.host, port: server.health.port, result: compactLocalResult(result) };
|
||||
|
||||
@@ -285,6 +285,7 @@ export function readManualBindingSources(value: unknown, key: string): CodexPool
|
||||
kind: readManualBindingKind(rawSource.kind, `${path}.kind`),
|
||||
provider: readManualBindingProvider(rawSource.provider, `${path}.provider`),
|
||||
description: readManualAccountReason(rawSource.description, `${path}.description`),
|
||||
hostProxyRef: readManualBindingHostProxyRef(rawSource.hostProxyRef, `${path}.hostProxyRef`),
|
||||
fixedProxy: null,
|
||||
};
|
||||
if (source.kind === "proxy" && source.provider === "fixed-http-proxy") {
|
||||
@@ -359,6 +360,16 @@ export function readManualBindingProvider(value: unknown, key: string): CodexPoo
|
||||
return text;
|
||||
}
|
||||
|
||||
export function readManualBindingHostProxyRef(value: unknown, key: string): string | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = stringValue(value)?.trim() ?? "";
|
||||
if (text.length === 0) return null;
|
||||
if (!/^config\/platform-infra\/host-proxy\.yaml#targets\.[A-Za-z0-9_-]+$/u.test(text)) {
|
||||
throw new Error(`${codexPoolConfigPath}.${key} must point at config/platform-infra/host-proxy.yaml#targets.<id>`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export function validateManualBindingSourceId(value: string, key: string): void {
|
||||
if (!/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported source id format`);
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ export function manualBindingSourcePlan(source: CodexPoolManualBindingSource): R
|
||||
kind: source.kind,
|
||||
provider: source.provider,
|
||||
description: source.description,
|
||||
hostProxyRef: source.hostProxyRef,
|
||||
fixedProxy: source.fixedProxy,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ export interface CodexPoolManualBindingSource {
|
||||
kind: CodexPoolManualBindingKind;
|
||||
provider: CodexPoolManualBindingProvider;
|
||||
description: string | null;
|
||||
hostProxyRef: string | null;
|
||||
fixedProxy: CodexPoolManualFixedProxyConfig | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user