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.
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.
+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
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
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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",
+27 -12
View File
@@ -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(),
]);