feat: panel-hosted Reality keys (keyStore, payload keyMaterial, backfill, cleanup)

This commit is contained in:
aaaamoooo
2026-10-20 07:20:42 +00:00
parent 1b77c610f4
commit 483b891306
6 changed files with 343 additions and 6 deletions
+46 -1
View File
@@ -315,6 +315,51 @@ function buildClientUri({ protocol, relayServer, relayPort, credentials, reality
return null;
}
// ---------- Panel-hosted Reality keys ----------
// Reality keypairs live in the panel database (data.keyStore) so that a node
// OS reinstall redeploys the SAME pair and client subscriptions never change.
// Encoding is exactly the agent-side format: Node JWK export of an x25519
// pair, whose base64url no-padding `d`/`x` values match
// `sing-box generate reality-keypair` output byte for byte.
function generateRealityKeyPair() {
const kp = crypto.generateKeyPairSync('x25519');
const priv = kp.privateKey.export({ format: 'jwk' });
const pub = kp.publicKey.export({ format: 'jwk' });
if (!priv.d || !pub.x) throw new Error('X25519 密钥生成失败');
return { privateKey: priv.d, publicKey: pub.x };
}
function realityKeyIdForNode(node) {
return (node.reality && node.reality.keyId) || `reality-${node.id}`;
}
// Returns the stored keypair for a Reality node, generating and storing one
// on first use. Non-Reality nodes return null.
function ensureRealityKey(data, node) {
if (!isRealityProtocol(node.protocol)) return null;
ensureNodeSecrets(node);
const keyId = realityKeyIdForNode(node);
data.keyStore ||= {};
let entry = data.keyStore[keyId];
if (!entry || !entry.privateKey || !entry.publicKey) {
entry = { ...generateRealityKeyPair(), updatedAt: new Date().toISOString() };
data.keyStore[keyId] = entry;
}
return entry;
}
// Payload fragment for apply_resource jobs: { "<keyId>": { privateKey, publicKey } }.
function keyMaterialForNode(data, node) {
const entry = ensureRealityKey(data, node);
if (!entry) return null;
return { [realityKeyIdForNode(node)]: { privateKey: entry.privateKey, publicKey: entry.publicKey } };
}
function dropRealityKey(data, node) {
if (data.keyStore) delete data.keyStore[realityKeyIdForNode(node)];
}
function profile(role, protocol) { return PROFILE_CATALOG.find((item) => item.role === role && item.id === protocol); }
function validateEntryProtocol(protocol) {
const item = profile('relay-ingress', protocol);
@@ -330,5 +375,5 @@ function isRealityProtocol(protocol) { return ['vless-reality-vision', 'vless-re
module.exports = {
PROFILE_CATALOG, REALITY_PRESETS, SHADOWSOCKS_METHODS, nodeEmail, newNodeClientCredentials,
buildNodeResource, buildNodeExitResource, ensureNodeSecrets, buildClientUri, validateEntryProtocol, validateProtocolPair, isRealityProtocol,
resolveBindAddress
resolveBindAddress, generateRealityKeyPair, realityKeyIdForNode, ensureRealityKey, keyMaterialForNode, dropRealityKey
};