Allow self-hosted relays to opt into wss:// (#767)
* Add relay TLS schema fields * Wire relay TLS into websocket URLs * Propagate relay TLS from pairing offers * Expose relay TLS opt-in * Mark relay TLS fallback for removal * Preserve explicit relay TLS false * Align relay TLS defaults outside config loader
This commit is contained in:
@@ -178,6 +178,7 @@ export default function PairScanScreen() {
|
||||
id: "probe",
|
||||
type: "relay",
|
||||
relayEndpoint: normalizeHostPort(offer.relay.endpoint),
|
||||
useTls: offer.relay.useTls,
|
||||
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
||||
},
|
||||
{ serverId: offer.serverId },
|
||||
|
||||
@@ -137,6 +137,7 @@ export function PairLinkModal({ visible, onClose, onCancel, onSaved }: PairLinkM
|
||||
id: "probe",
|
||||
type: "relay",
|
||||
relayEndpoint: normalizeHostPort(parsedOffer.relay.endpoint),
|
||||
useTls: parsedOffer.relay.useTls,
|
||||
daemonPublicKeyB64: parsedOffer.daemonPublicKeyB64,
|
||||
},
|
||||
{ serverId: parsedOffer.serverId },
|
||||
|
||||
@@ -198,6 +198,7 @@ function makeOffer(input?: Partial<ConnectionOffer>): ConnectionOffer {
|
||||
daemonPublicKeyB64: input?.daemonPublicKeyB64 ?? "pk_test_offer",
|
||||
relay: {
|
||||
endpoint: input?.relay?.endpoint ?? "relay.paseo.sh:443",
|
||||
useTls: input?.relay?.useTls ?? false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1616,6 +1617,43 @@ describe("HostRuntimeStore", () => {
|
||||
store.syncHosts([]);
|
||||
});
|
||||
|
||||
it("stores relay TLS from a pairing offer", async () => {
|
||||
const store = new HostRuntimeStore({
|
||||
deps: {
|
||||
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
|
||||
connectToDaemon: async ({ host }) => ({
|
||||
client: makeConnectedProbeClient(5) as unknown as DaemonClient,
|
||||
serverId: host.serverId,
|
||||
hostname: host.label ?? null,
|
||||
}),
|
||||
getClientId: async () => "cid_test_runtime",
|
||||
},
|
||||
});
|
||||
|
||||
await store.upsertConnectionFromOffer(
|
||||
makeOffer({
|
||||
relay: {
|
||||
endpoint: "relay.example.com:443",
|
||||
useTls: true,
|
||||
},
|
||||
}),
|
||||
"tls relay",
|
||||
);
|
||||
|
||||
const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer");
|
||||
expect(pairedHost?.connections).toEqual([
|
||||
{
|
||||
id: "relay:wss:relay.example.com:443",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.example.com:443",
|
||||
useTls: true,
|
||||
daemonPublicKeyB64: "pk_test_offer",
|
||||
},
|
||||
]);
|
||||
|
||||
store.syncHosts([]);
|
||||
});
|
||||
|
||||
it("uses the latest advertised hostname when re-pairing an existing relay host", async () => {
|
||||
const store = new HostRuntimeStore({
|
||||
deps: {
|
||||
|
||||
@@ -476,7 +476,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
...base,
|
||||
url: buildRelayWebSocketUrl({
|
||||
endpoint: connection.relayEndpoint,
|
||||
useTls: shouldUseTlsForDefaultHostedRelay(connection.relayEndpoint),
|
||||
useTls: connection.useTls ?? shouldUseTlsForDefaultHostedRelay(connection.relayEndpoint),
|
||||
serverId: host.serverId,
|
||||
}),
|
||||
e2ee: {
|
||||
@@ -1414,21 +1414,25 @@ export class HostRuntimeStore {
|
||||
async upsertRelayConnection(input: {
|
||||
serverId: string;
|
||||
relayEndpoint: string;
|
||||
useTls?: boolean;
|
||||
daemonPublicKeyB64: string;
|
||||
label?: string;
|
||||
}): Promise<HostProfile> {
|
||||
const relayEndpoint = normalizeHostPort(input.relayEndpoint);
|
||||
const useTls = input.useTls ?? false;
|
||||
const daemonPublicKeyB64 = input.daemonPublicKeyB64.trim();
|
||||
if (!daemonPublicKeyB64) {
|
||||
throw new Error("daemonPublicKeyB64 is required");
|
||||
}
|
||||
const explicitUseTls = input.useTls !== undefined;
|
||||
return this.upsertHostConnection({
|
||||
serverId: input.serverId,
|
||||
label: input.label,
|
||||
connection: {
|
||||
id: `relay:${relayEndpoint}`,
|
||||
id: useTls ? `relay:wss:${relayEndpoint}` : `relay:${relayEndpoint}`,
|
||||
type: "relay",
|
||||
relayEndpoint,
|
||||
...(explicitUseTls ? { useTls } : {}),
|
||||
daemonPublicKeyB64,
|
||||
},
|
||||
});
|
||||
@@ -1438,6 +1442,7 @@ export class HostRuntimeStore {
|
||||
return this.upsertRelayConnection({
|
||||
serverId: offer.serverId,
|
||||
relayEndpoint: offer.relay.endpoint,
|
||||
useTls: offer.relay.useTls,
|
||||
daemonPublicKeyB64: offer.daemonPublicKeyB64,
|
||||
label,
|
||||
});
|
||||
@@ -1983,6 +1988,7 @@ export interface HostMutations {
|
||||
upsertRelayConnection: (input: {
|
||||
serverId: string;
|
||||
relayEndpoint: string;
|
||||
useTls?: boolean;
|
||||
daemonPublicKeyB64: string;
|
||||
label?: string;
|
||||
}) => Promise<HostProfile>;
|
||||
|
||||
@@ -27,4 +27,48 @@ describe("normalizeStoredHostProfile", () => {
|
||||
});
|
||||
expect(profile?.connections[0]).not.toHaveProperty("password");
|
||||
});
|
||||
|
||||
it("preserves legacy relay ids when TLS is absent", () => {
|
||||
const profile = normalizeStoredHostProfile({
|
||||
serverId: "srv_relay",
|
||||
connections: [
|
||||
{
|
||||
id: "relay:relay.example.com:80",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.example.com:80",
|
||||
daemonPublicKeyB64: "pubkey",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(profile?.connections[0]).toEqual({
|
||||
id: "relay:relay.example.com:80",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.example.com:80",
|
||||
daemonPublicKeyB64: "pubkey",
|
||||
});
|
||||
});
|
||||
|
||||
it("namespaces relay ids only when TLS is true", () => {
|
||||
const profile = normalizeStoredHostProfile({
|
||||
serverId: "srv_relay",
|
||||
connections: [
|
||||
{
|
||||
id: "relay:relay.example.com:443",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.example.com:443",
|
||||
useTls: true,
|
||||
daemonPublicKeyB64: "pubkey",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(profile?.connections[0]).toEqual({
|
||||
id: "relay:wss:relay.example.com:443",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.example.com:443",
|
||||
useTls: true,
|
||||
daemonPublicKeyB64: "pubkey",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface RelayHostConnection {
|
||||
id: string;
|
||||
type: "relay";
|
||||
relayEndpoint: string;
|
||||
useTls?: boolean;
|
||||
daemonPublicKeyB64: string;
|
||||
}
|
||||
|
||||
@@ -73,6 +74,7 @@ function hostConnectionEquals(left: HostConnection, right: HostConnection): bool
|
||||
if (left.type === "relay" && right.type === "relay") {
|
||||
return (
|
||||
left.relayEndpoint === right.relayEndpoint &&
|
||||
left.useTls === right.useTls &&
|
||||
left.daemonPublicKeyB64 === right.daemonPublicKeyB64
|
||||
);
|
||||
}
|
||||
@@ -243,7 +245,7 @@ function normalizeStoredConnection(connection: unknown): HostConnection | null {
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
const type = typeof record.type === "string" ? record.type : null;
|
||||
const type = record.type;
|
||||
if (type === "directTcp") {
|
||||
try {
|
||||
const endpoint = normalizeLoopbackToLocalhost(
|
||||
@@ -277,10 +279,12 @@ function normalizeStoredConnection(connection: unknown): HostConnection | null {
|
||||
typeof record.daemonPublicKeyB64 === "string" ? record.daemonPublicKeyB64 : ""
|
||||
).trim();
|
||||
if (!daemonPublicKeyB64) return null;
|
||||
const useTls = typeof record.useTls === "boolean" ? record.useTls : undefined;
|
||||
return {
|
||||
id: `relay:${relayEndpoint}`,
|
||||
id: useTls === true ? `relay:wss:${relayEndpoint}` : `relay:${relayEndpoint}`,
|
||||
type: "relay",
|
||||
relayEndpoint,
|
||||
...(useTls !== undefined ? { useTls } : {}),
|
||||
daemonPublicKeyB64,
|
||||
};
|
||||
} catch {
|
||||
|
||||
@@ -145,6 +145,37 @@ describe("test-daemon-connection connectToDaemon", () => {
|
||||
expect(daemonClientMock.createdConfigs[0]?.password).toBe("shared-secret");
|
||||
});
|
||||
|
||||
it("uses relay TLS from the stored connection", async () => {
|
||||
const mod = await import("./test-daemon-connection");
|
||||
|
||||
const tlsResult = await mod.connectToDaemon(
|
||||
{
|
||||
id: "relay:wss:[::1]:443",
|
||||
type: "relay",
|
||||
relayEndpoint: "[::1]:443",
|
||||
useTls: true,
|
||||
daemonPublicKeyB64: "pubkey",
|
||||
},
|
||||
{ serverId: "srv_probe_test" },
|
||||
);
|
||||
await tlsResult.client.close();
|
||||
|
||||
const plainResult = await mod.connectToDaemon(
|
||||
{
|
||||
id: "relay:relay.paseo.sh:443",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.paseo.sh:443",
|
||||
useTls: false,
|
||||
daemonPublicKeyB64: "pubkey",
|
||||
},
|
||||
{ serverId: "srv_probe_test" },
|
||||
);
|
||||
await plainResult.client.close();
|
||||
|
||||
expect(daemonClientMock.createdConfigs[0]?.url).toMatch(/^wss:\/\/\[::1\]\/ws\?/);
|
||||
expect(daemonClientMock.createdConfigs[1]?.url).toMatch(/^ws:\/\/relay\.paseo\.sh:443\/ws\?/);
|
||||
});
|
||||
|
||||
it("surfaces auth rejection as an incorrect password", async () => {
|
||||
const mod = await import("./test-daemon-connection");
|
||||
daemonClientMock.setNextConnectFailure(
|
||||
|
||||
@@ -110,7 +110,7 @@ export async function buildClientConfig(
|
||||
...base,
|
||||
url: buildRelayWebSocketUrl({
|
||||
endpoint: connection.relayEndpoint,
|
||||
useTls: shouldUseTlsForDefaultHostedRelay(connection.relayEndpoint),
|
||||
useTls: connection.useTls ?? shouldUseTlsForDefaultHostedRelay(connection.relayEndpoint),
|
||||
serverId,
|
||||
}),
|
||||
e2ee: { enabled: true, daemonPublicKeyB64: connection.daemonPublicKeyB64 },
|
||||
|
||||
Reference in New Issue
Block a user