diff --git a/.gitignore b/.gitignore
index aea3f20..f72fd21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ feature_tests/.tmp
 *.log
 /site/public
 .vercel
+.proxy
diff --git a/e2e/shellcode/script.ts b/e2e/shellcode/script.ts
index 142f88f..2b118d4 100644
--- a/e2e/shellcode/script.ts
+++ b/e2e/shellcode/script.ts
@@ -69,6 +69,7 @@ class Script {
             .filter(Boolean)
             .join(path.delimiter),
           FNM_DIR: this.config.fnmDir,
+          FNM_NODE_DIST_MIRROR: "http://localhost:8080",
         }
 
         delete newProcessEnv.NODE_OPTIONS
diff --git a/jest.config.cjs b/jest.config.cjs
index 10e45aa..e15f18a 100644
--- a/jest.config.cjs
+++ b/jest.config.cjs
@@ -1,9 +1,12 @@
 /** @type {import('ts-jest').JestConfigWithTsJest} */
 module.exports = {
   preset: "ts-jest/presets/default-esm",
+  globalSetup: "./jest.global-setup.js",
+  globalTeardown: "./jest.global-teardown.js",
   testEnvironment: "node",
   testTimeout: 120000,
   extensionsToTreatAsEsm: [".ts"],
+  testPathIgnorePatterns: ["/node_modules/", "/dist/", "/target/"],
   moduleNameMapper: {
     "^(\\.{1,2}/.*)\\.js$": "$1",
     "#ansi-styles": "ansi-styles/index.js",
diff --git a/jest.global-setup.js b/jest.global-setup.js
new file mode 100644
index 0000000..7c52a6b
--- /dev/null
+++ b/jest.global-setup.js
@@ -0,0 +1,5 @@
+import { server } from "./tests/proxy-server/index.mjs"
+
+export default function () {
+  server.listen(8080)
+}
diff --git a/jest.global-teardown.js b/jest.global-teardown.js
new file mode 100644
index 0000000..d16c255
--- /dev/null
+++ b/jest.global-teardown.js
@@ -0,0 +1,5 @@
+import { server } from "./tests/proxy-server/index.mjs"
+
+export default () => {
+  server.close()
+}
diff --git a/tests/proxy-server/index.mjs b/tests/proxy-server/index.mjs
new file mode 100644
index 0000000..b306a6f
--- /dev/null
+++ b/tests/proxy-server/index.mjs
@@ -0,0 +1,65 @@
+// @ts-check
+
+import { createServer } from "node:http"
+import path from "node:path"
+import fs from "node:fs"
+import crypto from "node:crypto"
+import fetch from "node-fetch"
+import chalk from "chalk"
+
+const baseDir = path.join(process.cwd(), ".proxy")
+try {
+  fs.mkdirSync(baseDir, { recursive: true })
+} catch (e) {}
+
+/** @type {Map<string, Promise<{ headers: Record<string, string>, body: ArrayBuffer }>>} */
+const cache = new Map()
+
+export const server = createServer((req, res) => {
+  const pathname = req.url ?? "/"
+  const hash = crypto
+    .createHash("sha1")
+    .update(pathname ?? "/")
+    .digest("hex")
+  const extension = path.extname(pathname)
+  const filename = path.join(baseDir, hash) + extension
+  const headersFilename = path.join(baseDir, hash) + ".headers.json"
+  try {
+    const headers = JSON.parse(fs.readFileSync(headersFilename, "utf-8"))
+    const body = fs.createReadStream(filename)
+    console.log(chalk.green.dim(`[proxy] hit: ${pathname} -> ${filename}`))
+    res.writeHead(200, headers)
+    body.pipe(res)
+  } catch {
+    let promise = cache.get(filename)
+    if (!promise) {
+      console.log(chalk.red.dim(`[proxy] miss: ${pathname} -> ${filename}`))
+      promise = fetch(
+        "https://nodejs.org/dist/" + pathname.replace(/^\/+/, ""),
+        {
+          compress: false,
+        }
+      ).then(async (response) => {
+        const headers = Object.fromEntries(response.headers.entries())
+        const body = await response.arrayBuffer()
+        fs.writeFileSync(headersFilename, JSON.stringify(headers))
+        fs.writeFileSync(filename, Buffer.from(body))
+        return { headers, body }
+      })
+      cache.set(filename, promise)
+      promise.finally(() => cache.delete(filename))
+    }
+
+    promise.then(
+      ({ headers, body }) => {
+        res.writeHead(200, headers)
+        res.end(Buffer.from(body))
+      },
+      (err) => {
+        console.error(err)
+        res.writeHead(500)
+        res.end()
+      }
+    )
+  }
+})