@nordbyte/nordrelay 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -378,6 +378,15 @@ nordrelay peer invite --name workstation --scopes inspect,sessions.read,sessions
|
|
|
378
378
|
nordrelay peer add https://workstation.example:31979 --code one-time-code
|
|
379
379
|
```
|
|
380
380
|
|
|
381
|
+
If the controlling host should also be reachable by the remote peer, enable its peer server and set
|
|
382
|
+
`NORDRELAY_PEER_PUBLIC_URL` before running `peer add`, or pass it explicitly:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
nordrelay peer add https://workstation.example:31979 --code one-time-code --public-url https://controller.example:31979
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
NordRelay validates the advertised public URL against the peer identity endpoint during pairing and stores the current TLS certificate fingerprint for later reachability probes.
|
|
389
|
+
|
|
381
390
|
7. Confirm the connection:
|
|
382
391
|
|
|
383
392
|
```bash
|
package/dist/peer-client.js
CHANGED
|
@@ -35,6 +35,42 @@ export async function checkPeerEndpoint(url, options = {}) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
export async function checkPeerIdentityEndpoint(url, options = {}) {
|
|
39
|
+
const target = joinPeerUrl(url, "/peer/identity");
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
try {
|
|
42
|
+
const result = await requestJson({
|
|
43
|
+
url: target,
|
|
44
|
+
method: "GET",
|
|
45
|
+
expectedTlsFingerprint: options.expectedTlsFingerprint,
|
|
46
|
+
allowSelfSigned: true,
|
|
47
|
+
timeoutMs: options.timeoutMs ?? 4_000,
|
|
48
|
+
});
|
|
49
|
+
const identity = parsePeerIdentity(result.data?.identity);
|
|
50
|
+
const protocolVersion = result.data?.protocolVersion;
|
|
51
|
+
return {
|
|
52
|
+
ok: Boolean(identity) && protocolVersion === PEER_PROTOCOL_VERSION,
|
|
53
|
+
status: "reachable",
|
|
54
|
+
url: target,
|
|
55
|
+
latencyMs: Date.now() - startedAt,
|
|
56
|
+
statusCode: result.statusCode,
|
|
57
|
+
tlsFingerprint: result.tlsFingerprint,
|
|
58
|
+
identity,
|
|
59
|
+
detail: identity && protocolVersion === PEER_PROTOCOL_VERSION
|
|
60
|
+
? "Peer identity endpoint is reachable."
|
|
61
|
+
: "Endpoint responded, but did not return the expected peer identity payload.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
status: "unreachable",
|
|
68
|
+
url: target,
|
|
69
|
+
latencyMs: Date.now() - startedAt,
|
|
70
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
38
74
|
export async function pairPeer(options, identity, store = new PeerStore()) {
|
|
39
75
|
const timestamp = new Date().toISOString();
|
|
40
76
|
const payload = createPairingSignaturePayload(identity.public.nodeId, timestamp, options.code);
|
|
@@ -65,7 +101,7 @@ export async function pairPeer(options, identity, store = new PeerStore()) {
|
|
|
65
101
|
nodeId: result.data.identity.nodeId,
|
|
66
102
|
publicKey: result.data.identity.publicKey,
|
|
67
103
|
fingerprint: result.data.identity.fingerprint,
|
|
68
|
-
tlsFingerprint: result.tlsFingerprint,
|
|
104
|
+
tlsFingerprint: result.tlsFingerprint ?? null,
|
|
69
105
|
secret: result.data.secret,
|
|
70
106
|
enabled: true,
|
|
71
107
|
direction: "outbound",
|
|
@@ -286,3 +322,23 @@ function normalizePeerUrl(value) {
|
|
|
286
322
|
function normalizeFingerprint(value) {
|
|
287
323
|
return value?.trim().toLowerCase();
|
|
288
324
|
}
|
|
325
|
+
function parsePeerIdentity(value) {
|
|
326
|
+
if (!value || typeof value !== "object") {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
const record = value;
|
|
330
|
+
if (typeof record.nodeId !== "string" ||
|
|
331
|
+
typeof record.name !== "string" ||
|
|
332
|
+
typeof record.publicKey !== "string" ||
|
|
333
|
+
typeof record.fingerprint !== "string" ||
|
|
334
|
+
typeof record.createdAt !== "string") {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
nodeId: record.nodeId,
|
|
339
|
+
name: record.name,
|
|
340
|
+
publicKey: record.publicKey,
|
|
341
|
+
fingerprint: record.fingerprint,
|
|
342
|
+
createdAt: record.createdAt,
|
|
343
|
+
};
|
|
344
|
+
}
|
package/dist/peer-server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { URL } from "node:url";
|
|
|
4
4
|
import { friendlyErrorText } from "./error-messages.js";
|
|
5
5
|
import { createPairingSignaturePayload, createSharedSecret, ensurePeerTlsFiles, fingerprintForPublicKey, loadOrCreatePeerIdentity, verifyPeerPayload, } from "./peer-identity.js";
|
|
6
6
|
import { header, PeerNonceCache, verifyPeerRequest } from "./peer-auth.js";
|
|
7
|
+
import { checkPeerIdentityEndpoint } from "./peer-client.js";
|
|
7
8
|
import { peerRuntimeContextKey } from "./peer-context.js";
|
|
8
9
|
import { PeerStore } from "./peer-store.js";
|
|
9
10
|
import { PeerRuntimeService, peerError } from "./peer-runtime-service.js";
|
|
@@ -78,7 +79,7 @@ export async function startPeerServer(options) {
|
|
|
78
79
|
if (req.method === "POST" && url.pathname === "/peer/pair") {
|
|
79
80
|
const bodyText = await readBody(req, 128 * 1024);
|
|
80
81
|
const body = parseJson(bodyText);
|
|
81
|
-
const response = handlePair(body);
|
|
82
|
+
const response = await handlePair(body);
|
|
82
83
|
sendJson(res, 201, response);
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
@@ -123,7 +124,7 @@ export async function startPeerServer(options) {
|
|
|
123
124
|
sendJson(res, status, { error: friendlyErrorText(error) });
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
function handlePair(body) {
|
|
127
|
+
async function handlePair(body) {
|
|
127
128
|
if (!body?.identity?.nodeId || !body.identity.publicKey || !body.code || !body.signature || !body.timestamp) {
|
|
128
129
|
throw new Error("Invalid peer pairing request.");
|
|
129
130
|
}
|
|
@@ -140,17 +141,20 @@ export async function startPeerServer(options) {
|
|
|
140
141
|
if (!verifyPeerPayload(body.identity.publicKey, signaturePayload, body.signature)) {
|
|
141
142
|
throw new Error("Invalid peer pairing signature.");
|
|
142
143
|
}
|
|
144
|
+
const publicUrl = body.publicUrl?.trim() || undefined;
|
|
145
|
+
const publicUrlTlsFingerprint = publicUrl ? await verifyPairingPublicUrl(publicUrl, body.identity) : undefined;
|
|
143
146
|
const invitation = store.consumeInvitation(body.code, body.identity.nodeId);
|
|
144
147
|
const secret = createSharedSecret();
|
|
145
148
|
const peer = store.upsertPeer({
|
|
146
149
|
name: body.name?.trim() || body.identity.name || invitation.name,
|
|
147
|
-
url:
|
|
150
|
+
url: publicUrl,
|
|
148
151
|
nodeId: body.identity.nodeId,
|
|
149
152
|
publicKey: body.identity.publicKey,
|
|
150
153
|
fingerprint: body.identity.fingerprint,
|
|
154
|
+
tlsFingerprint: publicUrl ? publicUrlTlsFingerprint ?? null : undefined,
|
|
151
155
|
secret,
|
|
152
156
|
enabled: true,
|
|
153
|
-
direction:
|
|
157
|
+
direction: publicUrl ? "bidirectional" : "inbound",
|
|
154
158
|
scopes: invitation.scopes,
|
|
155
159
|
allowedAgents: invitation.allowedAgents,
|
|
156
160
|
allowedWorkspaceRoots: invitation.allowedWorkspaceRoots,
|
|
@@ -167,6 +171,18 @@ export async function startPeerServer(options) {
|
|
|
167
171
|
workspaceAliases: peer.workspaceAliases,
|
|
168
172
|
};
|
|
169
173
|
}
|
|
174
|
+
async function verifyPairingPublicUrl(publicUrl, expectedIdentity) {
|
|
175
|
+
const probe = await checkPeerIdentityEndpoint(publicUrl, { timeoutMs: 4_000 });
|
|
176
|
+
if (!probe.ok || !probe.identity) {
|
|
177
|
+
throw new Error(`Peer public URL is not reachable or does not expose a valid NordRelay identity: ${probe.detail}`);
|
|
178
|
+
}
|
|
179
|
+
if (probe.identity.nodeId !== expectedIdentity.nodeId ||
|
|
180
|
+
probe.identity.publicKey !== expectedIdentity.publicKey ||
|
|
181
|
+
probe.identity.fingerprint !== expectedIdentity.fingerprint) {
|
|
182
|
+
throw new Error("Peer public URL identity does not match the pairing identity.");
|
|
183
|
+
}
|
|
184
|
+
return probe.tlsFingerprint;
|
|
185
|
+
}
|
|
170
186
|
function authenticate(req, method, pathname, body) {
|
|
171
187
|
const peerId = header(req, "x-nordrelay-peer-id");
|
|
172
188
|
const peer = peerId ? store.get(peerId) : null;
|
package/dist/peer-store.js
CHANGED
|
@@ -91,7 +91,9 @@ export class PeerStore {
|
|
|
91
91
|
existing.url = input.url ?? existing.url;
|
|
92
92
|
existing.publicKey = input.publicKey;
|
|
93
93
|
existing.fingerprint = input.fingerprint;
|
|
94
|
-
|
|
94
|
+
if (input.tlsFingerprint !== undefined) {
|
|
95
|
+
existing.tlsFingerprint = input.tlsFingerprint || undefined;
|
|
96
|
+
}
|
|
95
97
|
existing.secret = input.secret;
|
|
96
98
|
existing.enabled = input.enabled ?? existing.enabled;
|
|
97
99
|
existing.direction = mergeDirection(existing.direction, input.direction ?? existing.direction);
|
|
@@ -111,7 +113,7 @@ export class PeerStore {
|
|
|
111
113
|
nodeId: input.nodeId,
|
|
112
114
|
publicKey: input.publicKey,
|
|
113
115
|
fingerprint: input.fingerprint,
|
|
114
|
-
tlsFingerprint: input.tlsFingerprint,
|
|
116
|
+
tlsFingerprint: input.tlsFingerprint || undefined,
|
|
115
117
|
secret: input.secret,
|
|
116
118
|
enabled: input.enabled ?? true,
|
|
117
119
|
direction: input.direction ?? "outbound",
|
|
@@ -2166,7 +2166,8 @@
|
|
|
2166
2166
|
applyPermissions();
|
|
2167
2167
|
}
|
|
2168
2168
|
function openPeerAddDialog() {
|
|
2169
|
-
|
|
2169
|
+
const publicUrl = state.peers?.enabled ? state.peers?.listenUrl || "" : "";
|
|
2170
|
+
adminDialog("Add peer", '<label>Peer URL<input id="dlgPeerAddUrl" placeholder="https://host:31979"></label><label>Pairing code<input id="dlgPeerAddCode"></label><label>Name<input id="dlgPeerAddName" placeholder="optional local label"></label><label>Public URL for this node<input id="dlgPeerAddPublicUrl" placeholder="optional" value="' + attr(publicUrl) + '"></label>', async () => {
|
|
2170
2171
|
const r = await api("/api/peers/pair", { method: "POST", body: JSON.stringify({ url: val("dlgPeerAddUrl"), code: val("dlgPeerAddCode"), name: val("dlgPeerAddName") || void 0, publicUrl: val("dlgPeerAddPublicUrl") || void 0 }), local: true });
|
|
2171
2172
|
toast("Added peer " + (r.peer?.name || ""));
|
|
2172
2173
|
await loadPeers();
|
package/package.json
CHANGED
|
@@ -915,16 +915,19 @@ async function commandPeer(options) {
|
|
|
915
915
|
if (flags.subcommand === "add") {
|
|
916
916
|
const url = flags.url || await ask(null, "Peer URL", "");
|
|
917
917
|
const code = flags.code || await ask(null, "Pairing code", "");
|
|
918
|
+
const configuredPublicUrl = process.env.NORDRELAY_PEER_ENABLED === "true" ? process.env.NORDRELAY_PEER_PUBLIC_URL : undefined;
|
|
919
|
+
const publicUrl = flags.publicUrl || configuredPublicUrl;
|
|
918
920
|
const result = await clientMod.pairPeer({
|
|
919
921
|
url,
|
|
920
922
|
code,
|
|
921
923
|
name: flags.name,
|
|
922
|
-
publicUrl
|
|
924
|
+
publicUrl,
|
|
923
925
|
}, identity, store);
|
|
924
926
|
console.log(`Added peer ${result.peer.name} (${result.peer.id}).`);
|
|
925
927
|
console.log(`Node: ${result.peer.nodeId}`);
|
|
926
928
|
console.log(`Fingerprint: ${result.peer.fingerprint}`);
|
|
927
929
|
if (result.tlsFingerprint) console.log(`TLS fingerprint: ${result.tlsFingerprint}`);
|
|
930
|
+
if (publicUrl) console.log(`Shared public URL: ${publicUrl}`);
|
|
928
931
|
return;
|
|
929
932
|
}
|
|
930
933
|
|