A self-hosted DNS + HTTPS canary in a single Go binary. Mint a unique link, drop it anywhere, and watch exactly who — and what — touches it, in real time.
Tripflare is the authoritative DNS server and HTTPS server for your own domain. Every link it generates — abc123.yourdomain.com — is a tripwire. The moment anything resolves the name or fetches the URL, Tripflare records it (source IP, DNS resolver, EDNS client subnet, HTTP method, headers, User-Agent, path, and query payload) and streams it to a live dashboard.
Why own the DNS layer too? A plain tracking pixel only fires when something makes the HTTP request. But the DNS lookup happens first, separately, and through different infrastructure — the target's resolver, not the target itself. Sandboxes, AV scanners, corporate proxies, and DNS-only exfil channels routinely resolve a hostname without ever fetching the URL. Tripflare logs both stages, so you see the resolver and the client — even when only one of them ever shows up.
Each event lands in the dashboard the instant it happens — geo-enriched, with full request detail.
A Telegram link-preview bot caught unfurling a link. The two stages arrive from different Telegram IPs — the DNS query from 91.x.x.x, the HTTP fetch from 149.x.x.x — each tagged with its ASN and organization. That split is exactly what a DNS-blind tracking pixel can never show you.
A single binary runs three concurrent subsystems — a DNS server, an HTTP server, and an ACME manager — sharing one in-memory registry that's persisted to SQLite.
flowchart TD
main["main.go<br/>config · signals · flush loop"]
dns["DNS Server<br/>:53 UDP + TCP"]
http["HTTP Server<br/>:80 + :443"]
acme["ACME Manager<br/>(lego)"]
reg["Registry<br/>in-memory · pub/sub · ring buffer"]
chal["Challenge Store"]
db[("SQLite<br/>WAL")]
ui["Web UI + SSE<br/>/tripflare"]
main --> dns & http & acme
dns -->|record DNS events| reg
http -->|record HTTP events| reg
http --- ui
ui -->|live stream| reg
reg <-->|flush / load| db
acme -->|write TXT token| chal
dns -->|read TXT token| chal
acme -.->|GetCertificate| http
When a target touches a link, the reveal happens in two independent stages:
sequenceDiagram
autonumber
participant T as Target
participant D as DNS :53
participant H as HTTPS :443
participant R as Registry
participant U as Dashboard
Note over T,D: Stage 1 — DNS resolution
T->>D: A? abc123.yourdomain.com
D->>R: RecordDNS(slug, resolver IP, EDNS subnet)
D-->>T: A 203.0.113.1, TTL=1
R-->>U: live event (SSE)
Note over T,H: Stage 2 — HTTP fetch
T->>H: TLS handshake, wildcard cert
T->>H: GET /img.png?payload=x
H->>R: RecordHTTP(slug, client IP, UA, headers, query)
H-->>T: 1x1 transparent pixel
R-->>U: live event (SSE)
The two stages can come from different parties. The DNS query usually arrives from the target's resolver (an ISP or corporate DNS, sometimes far from the target); the HTTP fetch comes from the actual client. Seeing both is the whole point.
A few deliberate details:
TTL=1on every answer so resolvers don't cache — repeat visits keep showing up.- Wildcard TLS (
*.yourdomain.com) via Let's Encrypt DNS-01, so links are clean HTTPS with no cert warnings. The same binary answers the ACME challenge over DNS. - Open Graph meta tags on non-pixel responses, so pasting a link into a messenger triggers its preview bot to resolve and fetch — another capture.
- Tracking pixels (
.png/.gif/.jpg) return a 1x1 transparent image, ideal for emails and documents. - Per
IP:slugrate limiting and a ring buffer per slug keep a noisy or hostile visitor from running away with memory.
For the full component breakdown, schema, and lifecycle, see ARCHITECTURE.md.
HTTP API (all routes on :443, Basic Auth required)
| Method | Path | Description |
|---|---|---|
POST |
/api/slugs |
Create a tracker (owned by the authenticated user) |
GET |
/api/slugs |
List your trackers with event counts |
GET |
/api/slugs/{slug} |
Tracker detail + full event history (GeoIP-enriched) |
DELETE |
/api/slugs/{slug} |
Delete a tracker and its events |
GET |
/api/slugs/{slug}/events |
Server-Sent Events stream — history replay, then live |
DELETE |
/api/slugs/{slug}/events |
Clear a tracker's events |
GET |
/tripflare |
Embedded web UI |
The whole thing is curl-friendly:
# create a tracker
curl -u admin:pass -X POST https://yourdomain.com/api/slugs
# watch events live
curl -u admin:pass -N https://yourdomain.com/api/slugs/abc123/eventsEvery use case maps to a signal Tripflare captures:
- Out-of-band (OOB) interaction testing — a personal, self-hosted alternative to Burp Collaborator / interactsh. Point blind SSRF, XXE, command injection, or SQLi callbacks at a unique slug and see the source resolve and connect.
- Canary tokens — embed a pixel or link in a sensitive document, repo, or share. If it's opened or leaked, you get the IP, resolver, and User-Agent.
- Email open tracking — drop the pixel into your own campaigns; it fires when the mail client loads images.
- DNS-leak & egress testing — verify whether a host, VPN, or network actually leaks DNS or reaches out over HTTP.
- Catching link-preview bots — paste a link into Telegram, Slack, or Discord and watch the unfurl bots resolve and fetch (Open Graph tags included).
- CTF & security training — OOB challenges and exfil channels without standing up infrastructure each time.
Tripflare is for authorized testing, your own infrastructure, and consenting parties. It is not for unauthorized surveillance, phishing, or exfiltration. See Responsible use.
You'll need: a registered domain you can set NS records on, and a VPS with a static public IP and ports 53 (UDP+TCP), 80, and 443 free.
git clone https://github.com/pilat/tripflare
cd tripflare
go build -o tripflare ./cmd/tripflare/
cp config.yaml.example config.yaml
# Edit: domain, external_ip, nameservers, auth
$EDITOR config.yaml
./tripflare -config config.yamlPort 53 is privileged. Either run as root, or grant the binary the capability to bind privileged ports:
sudo setcap cap_net_bind_service=+ep ./tripflareThen open https://yourdomain.com/tripflare, log in, and create your first tracker.
Running it as a real service? The quick start runs Tripflare in the foreground. For a production install — a prebuilt release binary, a hardened systemd unit, freeing port 53 from
systemd-resolved(the usual gotcha on Ubuntu/Debian), and optional GeoIP — see docs/deploy.md.
This is the one piece of setup that isn't optional: Tripflare must be the authoritative DNS server for your domain, so the internet asks it to resolve your tracking links.
- Get a VPS with a static public IP.
- At your registrar, create A records for two nameserver hostnames (e.g.
ns1.example.com,ns2.example.com) pointing to the VPS IP. - Set the domain's NS records to those nameserver hostnames.
- Put
domain,external_ip, andnameserversinconfig.yaml.
Verify the delegation before going further:
dig NS yourdomain.com # should list your nameservers
dig A test.yourdomain.com @your-vps-ip # should return external_ipAll API and UI access requires HTTP Basic Auth. Generate a bcrypt hash and add it to the config:
htpasswd -nbBC 10 "" "your-password" | cut -d: -f2auth:
- username: "admin"
password_hash: "$2y$10$..."Multiple users are supported, and each user only ever sees their own trackers.
Tripflare manages its own wildcard certificate (*.yourdomain.com + the apex) via the Let's Encrypt DNS-01 challenge — the built-in DNS server answers the challenge query itself, no external DNS provider needed.
- Start with
acme.staging: trueand confirm a certificate is issued. - Switch to
acme.staging: falsefor a trusted production certificate. - Certs live in
cert_pathand auto-renew when fewer than 30 days remain.
With acme.enabled: false, the server runs on a self-signed certificate (fine for local testing, browser warnings expected).
Tripflare enriches every event with country, flag, ASN, and organization when MaxMind-format databases are present. Download DB-IP Lite (or MaxMind GeoLite2) and drop the files into geoip_path:
geoip/
dbip-country-lite.mmdb
dbip-asn-lite.mmdb
Files are matched by keyword (*country*.mmdb, *asn*.mmdb). Missing databases silently disable enrichment — everything else still works.
config.yaml is the source of truth; environment variables override individual fields.
| Field | Env override | Default | Description |
|---|---|---|---|
domain |
TRIPFLARE_DOMAIN |
— | Your tracking domain (required) |
external_ip |
TRIPFLARE_EXTERNAL_IP |
— | Server's public IP (required) |
nameservers |
— | — | NS hostnames returned for NS/SOA queries |
log_format |
— | "json" |
"text" (human) or "json" |
sqlite_path |
TRIPFLARE_SQLITE_PATH |
"./tripflare.db" |
SQLite database path |
cert_path |
TRIPFLARE_CERT_PATH |
"./certs" |
TLS certificate storage |
acme.enabled |
TRIPFLARE_ACME_ENABLED |
false |
Enable ACME certificate management |
acme.email |
TRIPFLARE_ACME_EMAIL |
— | ACME account email (required if enabled) |
acme.staging |
— | true |
Use Let's Encrypt staging |
geoip_path |
TRIPFLARE_GEOIP_PATH |
— | Directory with .mmdb files |
limits.slug_ttl |
— | "12h" |
Slug expiration duration |
limits.max_events_per_slug |
— | 500 |
Ring-buffer size per slug |
limits.flush_interval |
— | "30s" |
Registry → SQLite flush interval |
limits.max_hits_per_slug_per_minute |
— | 60 |
Rate limit per IP:slug |
This is research tooling, not infrastructure for doing harm. Tripflare ships with structural limits that make it unsuitable for persistent abuse and keep a €1/month VPS healthy:
- Slug TTL — all data auto-expires (default: 12 hours).
- Rate limiting — a per-
IP:slugtoken bucket. - Event cap — 500 events per slug; oldest evicted (ring buffer).
- Authentication — every access path requires valid credentials.
- No bulk export — events are viewed live, not harvested.
Single-user security-research tooling, intended for authorized penetration testing, CTF competitions, and security education.
Use it to test your own infrastructure, in engagements where you have explicit authorization, or against parties who consent. It is not for unauthorized surveillance, phishing, data exfiltration, C2, or any activity without permission from everyone involved.
Anything that logs IP addresses carries responsibility. The single-user, self-hosted, short-retention design — slugs auto-expire, nothing is exported in bulk — keeps the data footprint minimal by default.
