diff --git a/CHANGELOG.md b/CHANGELOG.md index a3721d1..016f1c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,17 @@ 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. +## [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 ### 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`. - `/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. diff --git a/README.md b/README.md index 1cc742f..7d9032f 100644 --- a/README.md +++ b/README.md @@ -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 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 - 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 diff --git a/bot/package-lock.json b/bot/package-lock.json index 66e3c4b..5545d9f 100644 --- a/bot/package-lock.json +++ b/bot/package-lock.json @@ -1,12 +1,12 @@ { "name": "discordbot-cloudflarewarp", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "discordbot-cloudflarewarp", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "discord.js": "^14.16.3" diff --git a/bot/package.json b/bot/package.json index 4461b91..31831d2 100644 --- a/bot/package.json +++ b/bot/package.json @@ -1,6 +1,6 @@ { "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", "main": "src/index.js", "type": "module", diff --git a/bot/src/index.js b/bot/src/index.js index f330060..c257af7 100644 --- a/bot/src/index.js +++ b/bot/src/index.js @@ -1,4 +1,5 @@ import { execFile } from "node:child_process"; +import https from "node:https"; import { promisify } from "node:util"; import { Client, @@ -16,10 +17,10 @@ const GUILD_ID = process.env.GUILD_ID || ""; 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 -// request arrived over WARP. Hitting the edge by IP literal lets us force the -// address family so both the IPv4 and IPv6 egress IPs can be shown. -const TRACE_V4 = "https://1.1.1.1/cdn-cgi/trace"; -const TRACE_V6 = "https://[2606:4700:4700::1111]/cdn-cgi/trace"; +// request arrived over WARP. The request is made once per address family so +// both the IPv4 and the IPv6 Cloudflare egress IPs can be shown. +const TRACE_HOST = "www.cloudflare.com"; +const TRACE_PATH = "/cdn-cgi/trace"; if (!TOKEN) { console.error("[bot] DISCORD_TOKEN is not set. Exiting."); @@ -47,16 +48,30 @@ function parseTrace(text) { return data; } -async function fetchTrace(url) { - const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }); - if (!res.ok) throw new Error(`trace HTTP ${res.status}`); - return parseTrace(await res.text()); +/** GET the trace endpoint over a specific IP family (4 or 6). */ +function fetchTrace(family) { + return new Promise((resolve, reject) => { + 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. */ -async function traceFamily(url) { +async function traceFamily(family) { try { - const t = await fetchTrace(url); + const t = await fetchTrace(family); return { ok: t.warp === "on" || t.warp === "plus", ...t }; } catch (err) { return { ok: false, error: err.message }; @@ -81,8 +96,8 @@ let lastWarp = null; async function checkWarp(reason = "periodic") { const [v4, v6, cliStatus] = await Promise.all([ - traceFamily(TRACE_V4), - traceFamily(TRACE_V6), + traceFamily(4), + traceFamily(6), warpCliStatus(), ]);