fix: 让 D601 proxy 使用 vpn-server Hysteria2

This commit is contained in:
pikastech
2026-07-20 05:00:49 +02:00
parent 8dc700db8c
commit a05ba5844c
4 changed files with 96 additions and 7 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ D601/D518 结果必须分表记录:`STATE`、Job/run、duration、`APK/NPM/GO/
- 集群入口经 Windows 宿主内网 SSH 分发静态客户端到 guest。
- Windows staging 工件按 YAML URL 获取并校验 SHA-256Provider/trans 不搬运大二进制。
- 启动 `k3s-agent` 前必须验证外网 probe。
- WireGuard 已建立的 worker 通过隧道地址连接 control-plane `vpn-server`,不为 proxy 单独开放公网端口
- WireGuard 只承载集群三层网络;NAT 或链路不稳定的 worker proxy 消费 `vpn-server` 输出的 Hysteria2 client YAML
- containerd 直接通过 host proxy 访问上游 registry,不使用公共镜像站。
`sub2api-egress-proxy` 的持久化配置从 `config/platform-infra/sub2api.yaml` 渲染,运行面变更必须通过受控 CLI 落地。不要把一次性 `kubectl patch` 留作最终状态。
+17 -1
View File
@@ -67,6 +67,22 @@ sources:
binarySha256: 84b23a2304a6c4efa41dc027a78b7226a94ef6c63566b56a5a2721bdfe473446
podAccess:
enabled: false
managed-vpn-server-hysteria:
extends: templates.sources.singBoxHostClient
sourceType: vpn-server-hysteria-managed
benchmarkRef: local-vpn-server
sourceConfigRef: /root/vpn-server/output/client.yaml
client:
version: v1.12.0
upstreamUrl: https://github.com/SagerNet/sing-box/releases/download/v1.12.0/sing-box-1.12.0-linux-amd64.tar.gz
archiveCachePath: /root/.unidesk/.state/artifacts/platform-infra/sing-box-1.12.0-linux-amd64.tar.gz
archiveSha256: f59b1253ae0143997cb46915af30d14a33431dcbd7e39edacfcde8d73050faaf
archiveInstallPath: /var/lib/unidesk/artifacts/sing-box-1.12.0-linux-amd64.tar.gz
binaryMember: sing-box-1.12.0-linux-amd64/sing-box
binaryCachePath: /root/.unidesk/.state/artifacts/platform-infra/sing-box-1.12.0
binarySha256: 84b23a2304a6c4efa41dc027a78b7226a94ef6c63566b56a5a2721bdfe473446
podAccess:
enabled: false
jd01-real-deps-master-shadowsocks:
extends: templates.sources.singBoxHostClient
benchmarkRef: "pikasTech/unidesk#1110"
@@ -100,7 +116,7 @@ targets:
D601-VM:
extends: templates.targets.hostProxyClient
route: D601-VM
sourceRef: sources.wireguard-worker-vpn-server-shadowsocks
sourceRef: sources.managed-vpn-server-hysteria
files:
k3sSystemdDropIn: /etc/systemd/system/k3s-agent.service.d/10-unidesk-proxy.conf
env:
+1 -1
View File
@@ -24,7 +24,7 @@
- 集群 apply 先经宿主内网 SSH 分发零依赖 proxy client,确认它连接声明的 `vpn-server` 且外网 probe 成功,再启动或恢复 `k3s-agent`
- 集群 apply 在 proxy 前先收敛 worker WireGuard;内容一致时不强写,只有 unit 未运行或握手过期且隧道探针失败时才恢复 worker 接口。
- 大型 proxy 工件由 Windows 宿主按 YAML `upstreamUrl` 获取并校验 SHA-256Provider/trans 只承载控制命令,不作为大二进制数据面。
- WireGuard 已建立的 worker 应通过隧道地址连接 control-plane 上的 `vpn-server`,不要求为 proxy 单独开放公网端口
- WireGuard 只承载集群三层网络;NAT 或链路不稳定的 worker proxy 使用 `vpn-server` 输出的 Hysteria2 client YAML,不把应用出网 TCP 再套入 WireGuard TCP 路径
- proxy client 协议实现版本由 host-proxy YAML 声明,并与所连接的 `vpn-server` 协议实现版本保持一致。
- control-plane 节点配置只组合 `hwlab nodes control-plane infra k3s-config plan|status|apply`
- 该窄域只比较并安装 owning YAML 渲染的 k3s systemd drop-in
+77 -4
View File
@@ -43,7 +43,7 @@ interface HostProxyServer {
interface HostProxySource {
id: string;
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks" | "vpn-server-hysteria-existing";
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks" | "vpn-server-hysteria-existing" | "vpn-server-hysteria-managed";
serverRef: string;
benchmarkRef: string;
implementationRef: string;
@@ -52,6 +52,15 @@ interface HostProxySource {
sourceKey: string;
sourceFingerprint: string;
masterShadowsocks: MasterShadowsocksSourceSpec | null;
managedHysteria: {
server: string;
serverPort: number;
auth: string;
serverName: string;
insecure: boolean;
obfsType: string;
obfsPassword: string;
} | null;
client: HostProxyClient;
proxyUrl: string;
externalProbeUrl: string;
@@ -294,8 +303,9 @@ 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", "vpn-server-hysteria-existing"] as const);
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks", "vpn-server-hysteria-existing", "vpn-server-hysteria-managed"] as const);
if (sourceType === "vpn-server-hysteria-existing") return parseExistingHysteriaSource(id, raw, sourceType);
if (sourceType === "vpn-server-hysteria-managed") return parseManagedHysteriaSource(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) {
@@ -316,6 +326,7 @@ function parseSource(id: string, raw: Record<string, unknown>): HostProxySource
sourceKey: resolved.sourceKey,
sourceFingerprint: resolved.fingerprint,
masterShadowsocks: resolved.masterShadowsocks,
managedHysteria: null,
client,
proxyUrl,
externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`),
@@ -337,6 +348,49 @@ function parseSource(id: string, raw: Record<string, unknown>): HostProxySource
};
}
function parseManagedHysteriaSource(id: string, raw: Record<string, unknown>, sourceType: HostProxySource["sourceType"]): HostProxySource {
const label = `${configPath}.sources.${id}`;
const sourceConfigRef = absolutePathField(raw, "sourceConfigRef", label);
const sourceRaw = record(Bun.YAML.parse(readFileSync(sourceConfigRef, "utf8")), sourceConfigRef);
const [server, portText] = stringField(sourceRaw, "server", sourceConfigRef).split(":");
const tls = record(sourceRaw.tls, `${sourceConfigRef}.tls`);
const obfs = record(sourceRaw.obfs, `${sourceConfigRef}.obfs`);
const salamander = record(obfs.salamander, `${sourceConfigRef}.obfs.salamander`);
const client = managedClientSpec(record(raw.client, `${label}.client`), `${label}.client`);
const proxyUrl = urlField(raw, "proxyUrl", label);
const expectedProxyUrl = `http://${client.listenHost}:${client.listenPort}`;
if (proxyUrl !== expectedProxyUrl) throw new Error(`${label}.proxyUrl must be ${expectedProxyUrl}`);
const managedHysteria = {
server: server ?? "",
serverPort: Number(portText),
auth: stringField(sourceRaw, "auth", sourceConfigRef),
serverName: stringField(tls, "sni", `${sourceConfigRef}.tls`),
insecure: booleanField(tls, "insecure", `${sourceConfigRef}.tls`),
obfsType: stringField(obfs, "type", `${sourceConfigRef}.obfs`),
obfsPassword: stringField(salamander, "password", `${sourceConfigRef}.obfs.salamander`),
};
if (!managedHysteria.server || !Number.isInteger(managedHysteria.serverPort)) throw new Error(`${sourceConfigRef}.server must be host:port`);
const source = {
id,
sourceType,
serverRef: stringField(raw, "serverRef", label),
benchmarkRef: stringField(raw, "benchmarkRef", label),
implementationRef: stringField(raw, "implementationRef", label),
sourceConfigRef,
sourceRef: sourceConfigRef,
sourceKey: "auth,obfs.salamander.password",
sourceFingerprint: fingerprint({ sourceConfigRef, managedHysteria }),
masterShadowsocks: null,
managedHysteria,
client,
proxyUrl,
externalProbeUrl: urlField(raw, "externalProbeUrl", label),
expectedServer: `${managedHysteria.server}:${managedHysteria.serverPort}`,
fingerprint: "",
};
return { ...source, fingerprint: fingerprint({ sourceType, sourceConfigRef, sourceFingerprint: source.sourceFingerprint, client, proxyUrl }) };
}
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}`);
@@ -355,6 +409,7 @@ function parseExistingHysteriaSource(id: string, raw: Record<string, unknown>, s
sourceKey: "",
sourceFingerprint: fingerprint({ sourceConfigRef, expectedServer }),
masterShadowsocks: null,
managedHysteria: null,
client,
proxyUrl,
externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`),
@@ -625,6 +680,16 @@ function skippedMasterProxyServer(source: HostProxySource, mode: "apply" | "stat
}
function proxySecretMaterial(server: HostProxyServer, source: HostProxySource): HostProxySecretMaterial {
if (source.managedHysteria !== null) {
const clientConfigJson = renderSingBoxClientConfig(source, "");
return {
serverConfigJson: "{}\n",
clientConfigJson,
sourcePath: source.sourceConfigRef,
fingerprint: fingerprintSecretValues({ clientConfigJson }, ["clientConfigJson"]),
valuesPrinted: false,
};
}
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"),
@@ -655,7 +720,7 @@ function renderMasterShadowsocksServerConfig(server: HostProxyServer, password:
}
function renderSingBoxClientConfig(source: HostProxySource, password: string): string {
if (source.masterShadowsocks === null || source.client.mode !== "trans-static-binary") {
if ((source.masterShadowsocks === null && source.managedHysteria === null) || source.client.mode !== "trans-static-binary") {
throw new Error(`${configPath}.sources.${source.id} cannot render sing-box client config`);
}
const client = source.client;
@@ -670,13 +735,21 @@ function renderSingBoxClientConfig(source: HostProxySource, password: string): s
experimental: { clash_api: { external_controller: client.clashApiListen } },
inbounds,
outbounds: [
{
source.managedHysteria === null ? {
type: "shadowsocks",
tag: "master-vpn",
server: source.masterShadowsocks.serverHost,
server_port: source.masterShadowsocks.serverPort,
method: source.masterShadowsocks.method,
password,
} : {
type: "hysteria2",
tag: "master-vpn",
server: source.managedHysteria.server,
server_port: source.managedHysteria.serverPort,
password: source.managedHysteria.auth,
obfs: { type: source.managedHysteria.obfsType, password: source.managedHysteria.obfsPassword },
tls: { enabled: true, server_name: source.managedHysteria.serverName, insecure: source.managedHysteria.insecure },
},
{ type: "direct", tag: "direct" },
{ type: "block", tag: "block" },