Fix IPv6 trace: use node:https with family option instead of IP literal fetch (1.1.1)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 01:23:28 +02:00
parent 60c1c8770e
commit 37e9af4080
5 changed files with 39 additions and 17 deletions
+8 -1
View File
@@ -3,10 +3,17 @@
All notable changes to this project are documented here, newest first. All notable changes to this project are documented here, newest first.
Every change is committed and pushed so the history can be followed in git as well. Every change is committed and pushed so the history can be followed in git as well.
## [1.1.1] - 2026-09-08
### Fixed
- IPv6 trace showed `n/a (fetch failed)`: Node's `fetch` (undici) mangles IPv6 literals during TLS
hostname verification. The check now uses `node:https` with the `family` option against
`www.cloudflare.com`, so both families work with normal certificate validation.
## [1.1.0] - 2026-09-08 ## [1.1.0] - 2026-09-08
### Changed ### Changed
- WARP check now queries Cloudflare over IPv4 and IPv6 separately (by edge IP literal) and logs both - WARP check now queries Cloudflare over IPv4 and IPv6 separately and logs both
egress IPs: `ipv4=104.28.x.x (warp=on) ipv6=2a09:bac1:... (warp=on) colo=AMS loc=NL`. egress IPs: `ipv4=104.28.x.x (warp=on) ipv6=2a09:bac1:... (warp=on) colo=AMS loc=NL`.
- `/warp` slash command shows the IPv4 and IPv6 Cloudflare IPs on separate lines. - `/warp` slash command shows the IPv4 and IPv6 Cloudflare IPs on separate lines.
- "Connected" now means the IPv4 path (the one Discord uses) reports `warp=on`; IPv6 is informational. - "Connected" now means the IPv4 path (the one Discord uses) reports `warp=on`; IPv6 is informational.
+1 -1
View File
@@ -27,7 +27,7 @@ generated inside a `node:22-bookworm-slim` container.
2. It then calls `https://www.cloudflare.com/cdn-cgi/trace` and **refuses to start the bot** unless 2. It then calls `https://www.cloudflare.com/cdn-cgi/trace` and **refuses to start the bot** unless
Cloudflare reports `warp=on` (or `warp=plus`). The egress IP and colo are logged. Cloudflare reports `warp=on` (or `warp=plus`). The egress IP and colo are logged.
3. `bot/src/index.js` logs into Discord and every `WARP_CHECK_INTERVAL` seconds (default 60) asks 3. `bot/src/index.js` logs into Discord and every `WARP_CHECK_INTERVAL` seconds (default 60) asks
Cloudflare's edge over IPv4 (`1.1.1.1`) and IPv6 (`2606:4700:4700::1111`) what it sees, logging a line like: Cloudflare once over IPv4 and once over IPv6 what it sees, logging a line like:
``` ```
[2026-09-08T00:20:15.123Z] [WARP] CONNECTED via Cloudflare (periodic) ipv4=104.28.x.x (warp=on) ipv6=2a09:bac1:xxxx::x:x (warp=on) colo=AMS loc=NL | warp-cli: Status update: Connected Network: healthy [2026-09-08T00:20:15.123Z] [WARP] CONNECTED via Cloudflare (periodic) ipv4=104.28.x.x (warp=on) ipv6=2a09:bac1:xxxx::x:x (warp=on) colo=AMS loc=NL | warp-cli: Status update: Connected Network: healthy
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "discordbot-cloudflarewarp", "name": "discordbot-cloudflarewarp",
"version": "1.1.0", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "discordbot-cloudflarewarp", "name": "discordbot-cloudflarewarp",
"version": "1.1.0", "version": "1.1.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"discord.js": "^14.16.3" "discord.js": "^14.16.3"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "discordbot-cloudflarewarp", "name": "discordbot-cloudflarewarp",
"version": "1.1.0", "version": "1.1.1",
"description": "Simple discord.js bot that runs behind a Cloudflare WARP tunnel and logs the Cloudflare IP it uses", "description": "Simple discord.js bot that runs behind a Cloudflare WARP tunnel and logs the Cloudflare IP it uses",
"main": "src/index.js", "main": "src/index.js",
"type": "module", "type": "module",
+27 -12
View File
@@ -1,4 +1,5 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
import https from "node:https";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { import {
Client, Client,
@@ -16,10 +17,10 @@ const GUILD_ID = process.env.GUILD_ID || "";
const CHECK_INTERVAL_SEC = Number(process.env.WARP_CHECK_INTERVAL || 60); const CHECK_INTERVAL_SEC = Number(process.env.WARP_CHECK_INTERVAL || 60);
// Cloudflare's trace endpoint reports the IP it sees us as and whether the // Cloudflare's trace endpoint reports the IP it sees us as and whether the
// request arrived over WARP. Hitting the edge by IP literal lets us force the // request arrived over WARP. The request is made once per address family so
// address family so both the IPv4 and IPv6 egress IPs can be shown. // both the IPv4 and the IPv6 Cloudflare egress IPs can be shown.
const TRACE_V4 = "https://1.1.1.1/cdn-cgi/trace"; const TRACE_HOST = "www.cloudflare.com";
const TRACE_V6 = "https://[2606:4700:4700::1111]/cdn-cgi/trace"; const TRACE_PATH = "/cdn-cgi/trace";
if (!TOKEN) { if (!TOKEN) {
console.error("[bot] DISCORD_TOKEN is not set. Exiting."); console.error("[bot] DISCORD_TOKEN is not set. Exiting.");
@@ -47,16 +48,30 @@ function parseTrace(text) {
return data; return data;
} }
async function fetchTrace(url) { /** GET the trace endpoint over a specific IP family (4 or 6). */
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }); function fetchTrace(family) {
if (!res.ok) throw new Error(`trace HTTP ${res.status}`); return new Promise((resolve, reject) => {
return parseTrace(await res.text()); const req = https.get(
{ host: TRACE_HOST, path: TRACE_PATH, family, timeout: 10_000 },
(res) => {
let body = "";
res.setEncoding("utf8");
res.on("data", (c) => (body += c));
res.on("end", () => {
if (res.statusCode !== 200) return reject(new Error(`trace HTTP ${res.statusCode}`));
resolve(parseTrace(body));
});
}
);
req.on("timeout", () => req.destroy(new Error("timeout")));
req.on("error", reject);
});
} }
/** Returns { ok, ip, warp, colo, loc, error } for one address family. */ /** Returns { ok, ip, warp, colo, loc, error } for one address family. */
async function traceFamily(url) { async function traceFamily(family) {
try { try {
const t = await fetchTrace(url); const t = await fetchTrace(family);
return { ok: t.warp === "on" || t.warp === "plus", ...t }; return { ok: t.warp === "on" || t.warp === "plus", ...t };
} catch (err) { } catch (err) {
return { ok: false, error: err.message }; return { ok: false, error: err.message };
@@ -81,8 +96,8 @@ let lastWarp = null;
async function checkWarp(reason = "periodic") { async function checkWarp(reason = "periodic") {
const [v4, v6, cliStatus] = await Promise.all([ const [v4, v6, cliStatus] = await Promise.all([
traceFamily(TRACE_V4), traceFamily(4),
traceFamily(TRACE_V6), traceFamily(6),
warpCliStatus(), warpCliStatus(),
]); ]);