mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 18:06:07 +00:00
fix(device-pair): mobile pairing rejects local IPv6 cleartext URLs (#101008)
* fix(device-pair): allow local IPv6 pairing URLs * test(device-pair): cover IPv6 cleartext lower bound
This commit is contained in:
@@ -847,6 +847,30 @@ describe("device-pair /pair default setup code", () => {
|
||||
expect(requireText(result)).toContain("Gateway: ws://192.168.1.20:18789");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"ws://[fc00::1]:18789",
|
||||
"ws://[fd7a:115c:a1e0::1]:18789",
|
||||
"ws://[fe80::1]:18789",
|
||||
"ws://[febf::1]:18789",
|
||||
])("allows IPv6 ULA and link-local cleartext setup url %s", async (publicUrl) => {
|
||||
const command = registerPairCommand({
|
||||
pluginConfig: {
|
||||
publicUrl,
|
||||
},
|
||||
});
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "",
|
||||
commandBody: "/pair",
|
||||
gatewayClientScopes: INTERNAL_SETUP_SCOPES,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(pluginApiMocks.issueDeviceBootstrapToken).toHaveBeenCalledTimes(1);
|
||||
expect(requireText(result)).toContain(`Gateway: ${publicUrl}`);
|
||||
});
|
||||
|
||||
it("allows mdns cleartext setup urls", async () => {
|
||||
const command = registerPairCommand({
|
||||
pluginConfig: {
|
||||
@@ -1015,6 +1039,30 @@ describe("device-pair /pair default setup code", () => {
|
||||
expect(requireText(result)).toContain("prefer gateway.tailscale.mode=serve");
|
||||
});
|
||||
|
||||
it.each(["ws://[2001:db8::1]:18789", "ws://[fe7f::1]:18789", "ws://[fec0::1]:18789"])(
|
||||
"rejects non-LAN IPv6 cleartext setup url %s before issuing setup codes",
|
||||
async (publicUrl) => {
|
||||
const command = registerPairCommand({
|
||||
pluginConfig: {
|
||||
publicUrl,
|
||||
},
|
||||
});
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "",
|
||||
commandBody: "/pair",
|
||||
gatewayClientScopes: INTERNAL_SETUP_SCOPES,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();
|
||||
expect(requireText(result)).toContain(
|
||||
"Tailscale and public mobile pairing require a secure gateway URL",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses Tailscale Serve MagicDNS as a secure setup url", async () => {
|
||||
vi.mocked(resolveTailnetHostWithRunner).mockResolvedValueOnce("gateway.tailnet.ts.net");
|
||||
const command = registerPairCommand({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Device Pair plugin entrypoint registers its OpenClaw integration.
|
||||
import { rm } from "node:fs/promises";
|
||||
import { isIP } from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
@@ -242,12 +243,24 @@ function isPrivateIPv4(address: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPrivateLanIPv6(address: string): boolean {
|
||||
if (isIP(address) !== 6) {
|
||||
return false;
|
||||
}
|
||||
const firstHextet = address.split(":", 1)[0] ?? "";
|
||||
if (!/^[0-9a-f]{4}$/.test(firstHextet)) {
|
||||
return false;
|
||||
}
|
||||
const value = Number.parseInt(firstHextet, 16);
|
||||
return (value & 0xfe00) === 0xfc00 || (value & 0xffc0) === 0xfe80;
|
||||
}
|
||||
|
||||
function isPrivateLanCleartextHost(host: string): boolean {
|
||||
const normalized = normalizeHostForIpCheck(host);
|
||||
if (normalized.endsWith(".local")) {
|
||||
return true;
|
||||
}
|
||||
if (isPrivateIPv4(normalized)) {
|
||||
if (isPrivateIPv4(normalized) || isPrivateLanIPv6(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const octets = parseIPv4Octets(normalized);
|
||||
|
||||
Reference in New Issue
Block a user