浏览器指纹伪造工具

PhantomRelay是一种高级浏览器中继爬虫,支持Chrome TLS/JA4指纹伪造、持久化配置文件、行为模拟和原生MCP服务器功能,适用于需要绕过反爬虫机制的数据采集场景。

  • by EfrainGaray
  • TypeScript
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 EfrainGaray/phantomrelay

使用说明

PhantomRelay

The world-class browser relay scraper. Chrome TLS/JA4 fingerprint at the BoringSSL level, persistent profiles with real history aging, behavioral simulation, and native MCP server for Claude.


Why PhantomRelay?

Most scrapers get blocked because they fail at the TLS layer — before sending a single HTTP header. PhantomRelay fixes this from the ground up:

Browserless Steel Browser PhantomRelay
TLS JA3 cipher order ❌ ❌ ✅
TLS JA4 extension order (BoringSSL) ❌ ❌ ✅ only one
HTTP/2 SETTINGS exact Chrome values ❌ Partial ✅
Profile aging (real Chrome SQLite history) ❌ ❌ ✅
Behavioral simulation (Bezier + Markov) ❌ ❌ ✅
Auto-escalation http→headless→stealth→human ❌ ❌ ✅
Native MCP server (Claude Desktop) ❌ ❌ ✅ only one

How it works

Request → Mode selection
            │
            ├─ http    → BoringSSL TLS (JA4 = Chrome 124) + H2 SETTINGS spoofing
            ├─ headless → Chrome + 11 stealth patches (CDP + JS IIFE)
            ├─ stealth  → headless + Bezier mouse + Markov typing + scroll sim
            └─ human    → stealth + aged profile (6mo history) + proxy rotation
                                        │
                              auto mode → escalates automatically on block

Quick start

Prerequisites

  • Node.js 24+
  • pnpm 9+
  • Rust toolchain (for JA4 native addon, optional)
git clone https://github.com/EfrainGaray/phantomrelay
cd phantomrelay
pnpm install
pnpm build

# Optional — compile the BoringSSL JA4 native addon (~75s, requires Rust)
cd packages/tls-native && npm run build && cd ../..

Start the relay server

cp .env.example .env
# edit .env as needed
pnpm --filter @phantomrelay/relay start

Using Docker (recommended for production)

docker compose -f docker/docker-compose.yml up -d
# relay on :3000, prometheus on :9091, grafana on :3001

API — REST

POST /scrape — Scrape a URL

# HTTP mode — fastest, TLS fingerprinting only
curl -X POST http://localhost:3000/scrape \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "mode": "http"}'

# Headless mode — real Chrome, stealth injected
curl -X POST http://localhost:3000/scrape \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "mode": "headless"}'

# Auto mode — escalates automatically if blocked
curl -X POST http://localhost:3000/scrape \
  -H "Content-Type: application/json" \
  -d '{"url": "https://protected-site.com", "mode": "auto"}'

Response:

{
  "taskId": "uuid",
  "status": "success",
  "mode": "http",
  "html": "<!DOCTYPE html>...",
  "latencyMs": 312
}

POST /scrape — With page actions

curl -X POST http://localhost:3000/scrape \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/login",
    "mode": "stealth",
    "actions": [
      { "type": "type", "selector": "#email", "text": "user@example.com" },
      { "type": "type", "selector": "#password", "text": "secret" },
      { "type": "click", "selector": "#submit" },
      { "type": "wait", "condition": { "type": "selector", "selector": ".dashboard" } }
    ],
    "extract": { "screenshot": true }
  }'

GET /sessions — List active browser sessions

curl http://localhost:3000/sessions

GET /metrics — Prometheus metrics

curl http://localhost:3000/metrics

API — MCP (Claude Desktop / Claude Code)

PhantomRelay is a native MCP server. Add it to Claude Desktop's config:

{
  "mcpServers": {
    "phantomrelay": {
      "command": "node",
      "args": ["/path/to/phantomrelay/apps/relay/dist/main.js", "--mcp"]
    }
  }
}

Then use it directly in Claude:

"Use PhantomRelay to scrape the pricing table from https://competitor.com"

Available MCP tools

scrape_url

Scrape a URL. Supports HTTP (TLS fingerprinting) and browser modes.

Parameters:
  url         string   required   URL to scrape
  mode        string   optional   "http" | "headless" | "stealth" | "human" | "auto"
  waitFor     string   optional   "load" | "domcontentloaded" | "networkidle"
  extractText boolean  optional   Include plain text (strips HTML tags)
  timeout     number   optional   Timeout in ms (default: 30000)

scrape_with_actions

Scrape a URL and interact with the page (click, type, scroll, wait).

Parameters:
  url     string   required   URL to scrape
  mode    string   optional   "stealth" | "human" (default: stealth)
  actions array    optional   Sequence of page actions
  timeout number   optional   Timeout in ms (default: 60000)

Action types:
  { type: "click",  selector: "#btn" }
  { type: "type",   selector: "#input", text: "hello" }
  { type: "scroll", y: 500 }
  { type: "wait",   condition: { type: "delay", ms: 1000 } }
  { type: "wait",   condition: { type: "selector", selector: ".loaded" } }

Programmatic usage (Node.js)

import { PhantomScraper } from '@phantomrelay/relay';
import { BrowserFleetManager } from '@phantomrelay/relay';
import { createLogger } from '@phantomrelay/observability';

const logger = createLogger({ name: 'my-scraper' });
const fleet = new BrowserFleetManager({ maxSessions: 5, logger });
const scraper = new PhantomScraper(fleet, logger);

// Simple HTTP scrape with JA4 fingerprint
const result = await scraper.scrape({
  id: 'task-1',
  url: 'https://example.com',
  mode: 'http',
  priority: 50,
  retries: 0,
  maxRetries: 3,
  createdAt: new Date(),
  options: {},
});

console.log(result.status);   // "success" | "blocked"
console.log(result.html);     // full page HTML
console.log(result.latencyMs); // e.g. 312

Auto-escalation

// Tries http → headless → stealth → human until success
const result = await scraper.scrape({
  id: 'task-2',
  url: 'https://cloudflare-protected-site.com',
  mode: 'auto',   // <-- auto-escalation
  priority: 50,
  retries: 0,
  maxRetries: 3,
  createdAt: new Date(),
  options: { timeout: 60_000 },
});

With proxy pool

import { ProxyPool } from '@phantomrelay/proxy-manager';

const proxyPool = new ProxyPool([
  'http://user:pass@proxy1.example.com:8080',
  'http://user:pass@proxy2.example.com:8080',
]);

const scraper = new PhantomScraper(fleet, logger, proxyPool);
// proxies are selected automatically with EMA health scoring
// failed/blocked proxies are banned per-domain automatically

Proxies apply to every evasion mode, including http: the HTTP client tunnels through the proxy with an HTTP CONNECT and performs its own TLS handshake over the tunnel, so the upstream sees the proxy's (e.g. residential) IP while the request keeps Chrome-shaped headers. This is essential for targets that block datacenter IPs (Reddit, X) regardless of TLS fingerprint. user:pass@ proxy auth is supported. Browser modes pass the same proxy to Chrome via --proxy-server.

Note: in http mode a configured proxy uses the Node TLS path (still JA3-ordered ciphers) rather than the native BoringSSL JA4 path — full JA4 + proxy is on the roadmap via the native addon's hyper-util proxy support.

With aged profiles

import { ProfileStore } from '@phantomrelay/profile-store';

const profileStore = new ProfileStore({ profilesDir: './data/profiles', logger });

// Create a profile with 6 months of fake Chrome history
await profileStore.create({ persona: 'developer' });

// The scraper automatically selects the healthiest profile per domain
const scraper = new PhantomScraper(fleet, logger, undefined, profileStore);

With CAPTCHA solving

import { CaptchaManager } from '@phantomrelay/captcha';

const captchaManager = new CaptchaManager({
  solvers: [{
    provider: '2captcha',
    apiKey: process.env.TWOCAPTCHA_API_KEY,
  }],
});

const scraper = new PhantomScraper(fleet, logger, undefined, undefined, captchaManager);

const result = await scraper.scrape({
  id: 'task-3',
  url: 'https://recaptcha-protected-site.com',
  mode: 'stealth',
  priority: 50,
  retries: 0,
  maxRetries: 3,
  createdAt: new Date(),
  options: { captchaSolve: true },
});

With page actions

const result = await scraper.scrape({
  id: 'task-4',
  url: 'https://spa-app.com',
  mode: 'stealth',
  priority: 50,
  retries: 0,
  maxRetries: 3,
  createdAt: new Date(),
  options: {
    actions: [
      { type: 'click', selector: '#load-more' },
      { type: 'wait', condition: { type: 'selector', selector: '.results' } },
      { type: 'scroll', y: 1000 },
    ],
    extract: { screenshot: true },
  },
});

console.log(result.screenshot); // base64 PNG

HAR recording

import { HarRecorder } from '@phantomrelay/browser-core';

// Attach to any active CDP session
const recorder = new HarRecorder(cdpSession);
await recorder.start();

// ... navigate, click, etc.

const har = await recorder.stop();
// har.entries = array of HarEntry (HAR 1.2 format)
fs.writeFileSync('session.har', JSON.stringify(har, null, 2));

Evasion modes in depth

Mode 1 — HTTP (~10-50ms)

  • Native BoringSSL (Chrome's own TLS library) via Rust addon
  • JA4 fingerprint: t13d1516h2_8daaf6152771_b0da82dd1658 (Chrome 124)
  • HTTP/2 SETTINGS: headerTableSize=65536, initialWindowSize=6291456, maxHeaderListSize=262144
  • HTTP/2 WINDOW_UPDATE: 15663105 (exact Chrome value)
  • Header order matching Chrome's network stack

Mode 2 — Headless (~200ms-2s)

  • Chrome launched with --headless=new (same renderer as headed)
  • --disable-blink-features=AutomationControlled
  • NO --disable-extensions (would eliminate chrome.runtime, detected by Cloudflare)
  • 11 JS stealth patches injected via single Page.addScriptToEvaluateOnNewDocument IIFE:
    • navigator.webdriver → undefined (not false — Chrome real doesn't have this property)
    • navigator.plugins → 3 real Chrome plugins
    • chrome.runtime → object with real methods
    • WebGL vendor/renderer → real GPU values
    • Canvas noise → deterministic per-session (same seed = same hash)
    • AudioContext → micro-noise in frequency data
    • window.cdc_* → removed from Object.getOwnPropertyNames
    • iframe webdriver leak → patched
    • Permissions API → normal browser behavior
    • outerWidth/outerHeight → fixed for headless=new
    • Performance.now() → micro-jitter

Mode 3 — Stealth (~2-15s)

  • All of Mode 2, plus:
  • Bezier curve mouse movement to interactive elements
  • Markov chain keyboard timing (WPM variance, inter-key delays)
  • Natural scroll simulation with momentum

Mode 4 — Human (~5-30s)

  • All of Mode 3, plus:
  • Aged Chrome profile with 6 months of real-looking browsing history
  • Proxy rotation with EMA health scoring
  • Random dwell time (1-3s) after navigation
  • clearFirst on type actions (Ctrl+A + Delete before typing)

Architecture

phantomrelay/
├── packages/
│   ├── types/            # Central interfaces (everything depends on this)
│   ├── observability/    # Pino logger + Prometheus metrics
│   ├── fingerprint/      # Consistent fingerprint generator (mulberry32 PRNG)
│   ├── stealth/          # 13 JS patches + Bezier/Markov behavioral simulation
│   ├── http-client/      # HTTP mode: H2-first, TLS cipher order, H2 SETTINGS
│   ├── browser-core/     # Raw CDP client, navigator, actions, extractor, HAR
│   ├── profile-store/    # Chrome profile persistence + SQLite history aging
│   ├── proxy-manager/    # Pool with EMA health scoring + per-domain banning
│   ├── captcha/          # Detection + 2captcha/CapMonster/AI Vision solvers
│   ├── task-queue/       # BullMQ priority queue + dead letter queue
│   └── tls-native/       # Rust/BoringSSL native addon — JA4 fingerprint
│
└── apps/
    └── relay/
        ├── src/
        │   ├── fleet-manager.ts      # Chrome session pool
        │   ├── relay-server.ts       # CDP WebSocket relay
        │   ├── scraper.ts            # Orchestration + auto-escalation
        │   ├── aging-scheduler.ts    # Background profile aging (BullMQ)
        │   ├── rate-limiter.ts       # Per-domain sliding window
        │   └── api/
        │       ├── rest.ts           # Fastify REST API
        │       ├── mcp.ts            # MCP server (stdio transport)
        │       └── register-mcp.ts   # MCP registration
        └── tests/                    # 896 tests total

Configuration

Copy .env.example to .env:

# Server
RELAY_PORT=3000
RELAY_HOST=0.0.0.0
RELAY_API_KEY=your-strong-api-key    # required in production

# Chrome fleet
CHROME_HEADLESS=true
CHROME_MAX_INSTANCES=5
CHROME_REMOTE_URL=                    # e.g. http://chrome:9222 in Docker
CHROME_IDLE_TIMEOUT_MS=300000

# Observability
LOG_LEVEL=info
LOG_PRETTY=false
METRICS_ENABLED=true
METRICS_PORT=9090

# Redis (for task queue + session registry)
REDIS_URL=redis://localhost:6379
REDIS_ENABLED=false

# Profiles
PROFILES_DIR=./data/profiles
AGING_ENABLED=false
AGING_INTERVAL_MS=21600000

# Proxy pool (comma-separated URLs)
PROXY_URLS=http://user:pass@proxy1.example.com:8080,http://user:pass@proxy2.example.com:8080
PROXY_STRATEGY=best-score             # best-score | round-robin | least-used | random

# CAPTCHA solving
CAPTCHA_PROVIDER=none                 # 2captcha | capmonster | ai_vision | none
CAPTCHA_API_KEY=

# MCP server (Claude Desktop / Claude Code)
MCP_ENABLED=false

# Rate limiting (requests per minute per endpoint)
RATE_LIMIT_SCRAPE=10
RATE_LIMIT_SESSION=30
RATE_LIMIT_DEFAULT=100

Stealth verification

# Run the CI gate against real anti-bot infrastructure
pnpm verify:stealth

# Output:
# ✓ httpbin.org — status 200
# ✓ httpbin.org — Chrome UA in response
# ✓ tls.peet.ws — JA4 t13d1516h2_8daaf6152771_b0da82dd1658
# ✓ httpbin.org/headers — sec-ch-ua present
# ✓ Fingerprint consistency — same seed = same UA

Running tests

pnpm test                     # all 896 tests
pnpm test --reporter=verbose  # with test names
pnpm test:coverage            # HTML coverage report
pnpm lint                     # ESLint (0 errors)
pnpm format                   # Prettier check
pnpm typecheck                # tsc --noEmit

Building the Rust TLS addon

The BoringSSL JA4 addon is optional but strongly recommended for production:

# Prerequisites: Rust toolchain (https://rustup.rs)
brew install cmake  # macOS
sudo apt install cmake  # Linux

cd packages/tls-native
npm run build         # release (optimized, ~75s first time)
npm run build:debug   # debug (faster, not for production)

# Verify
node __test__/smoke.test.mjs
# ✓ ja4=t13d1516h2_8daaf6152771_b0da82dd1658

Without the native addon, PhantomRelay falls back to Node.js TLS (correct cipher order, but no JA4 extension order control).


Grafana dashboard

Access at http://localhost:3001 (default credentials: admin / phantomrelay) after docker compose up.

Metrics tracked:

  • Requests per mode (http/headless/stealth/human)
  • Block rate per domain
  • Session pool utilization
  • Proxy health scores
  • CAPTCHA detection rate
  • p50/p95/p99 latency per mode

CI/CD

GitHub Actions pipeline on every push/PR:

Step Trigger Tool
Lint + format push, PR ESLint 9 + Prettier
Unit tests push, PR Vitest (896 tests)
Type check push, PR tsc --noEmit
Dependency audit push, PR pnpm audit
SAST push, PR CodeQL (javascript-typescript)
Stealth verification main push Real anti-bot sites
Docker build + health main push Multi-stage image
Image size report main push GitHub step summary

Project status

Phase: solid — validated product with real users, tests obligatory.

Area Status
Core scraping (4 modes) Production-ready
REST API Production-ready
MCP server Production-ready
Docker (multi-stage) Production-ready
Tests (896) Passing
ESLint 0 errors
Prettier Enforced
CI/CD Automated
GHCR push Pending
Docker Scout/Trivy Pending

License

MIT © Efrain Garay