Skip to content

pfoundation/peeringDBReminders

Repository files navigation

oix-peeringdb-reminder

Identifies IX members that are connected to the exchange fabric but missing from PeeringDB, and emails them a professional request to fix it — either by enabling Allow IXP Update or by creating/adding their PeeringDB listing. The first contact is an "initial" email; follow-ups are "reminder" emails. Every send is tracked in SQLite, so reminders never double-up and members who resolve their listing are dropped automatically.


Overview

An Internet Exchange operator can see exactly who is plugged into the fabric (via its IXP Manager), but PeeringDB — the database the rest of the internet uses to find peers — only shows networks that have listed themselves. The gap between those two lists is a set of members who are reachable on the exchange but invisible to potential peers.

This tool closes that gap:

  1. Pulls the authoritative member list (with contact emails and on-fabric details) from IXP Manager's keyed IX‑F export.
  2. Asks PeeringDB who is already listed at the exchange, plus per-network flags.
  3. Computes the missing members and picks the right call-to-action for each:
    • Enable update — the network already has a PeeringDB record, so it just needs to flip on Allow IXP Update (or add the exchange manually).
    • Create profile — the network has no PeeringDB record yet and must create one first.
  4. Sends one tailored email per missing ASN through Mailgun, and records every attempt in SQLite for deduplication and reminder scheduling.

Re-runs are safe and idempotent: a normal run only contacts members that have never been emailed; already-contacted members are re-contacted only through an explicit reminder run after the configured interval.

How it works

IXP Manager (IX-F export)        PeeringDB
   contact emails,                 who is already listed
   IPv4/IPv6, port speed           + per-net flags
            \                         /
             \                       /
              v                     v
            reconcile()  =  IX-F members
                            − PeeringDB-listed ASNs
                            − EXCLUDED_ASNS
                              |
                              v
                       select send batch
                  (pending / due-for-reminder)
                              |
                              v
                    render Handlebars templates
                       (subject / text / html)
                              |
                              v
                      Mailgun messages API
                              |
                              v
                  SQLite  (member_state + send_log)

Reconciliation rules:

missing   = IX-F members  −  PeeringDB-listed ASNs  −  EXCLUDED_ASNS
recipient = overrides[asn]  ??  IX-F contact_email  ??  (none → status 'no_contact')
variant   = has PeeringDB net record ? 'enable_update' : 'create_profile'

Reconciliation is pure (no writes); persistence upserts each missing member and marks any previously-tracked ASN that now appears in PeeringDB as resolved.

Requirements

  • Bun (developed against Bun 1.3.x). Bun runs the TypeScript sources directly — there is no build step.
  • A Mailgun account (API key + sending domain).
  • An IXP Manager instance with an API key that unlocks the keyed IX‑F export (contact emails).
  • A PeeringDB API key.

Install

bun install

Configuration

Copy the example env file and fill in your values. Secrets live in a gitignored .env, which Bun auto-loads.

cp .env.example .env
Variable Default Required Notes
OIX_PEERING_DB_REMINDER test no Send blocker. prod emails real members; any other value (or unset) is test mode.
TEST_RECIPIENT you@example.org no Where all mail is redirected in test mode.
IXP_MANAGER_BASE_URL http://ixp-manager.example no IXP Manager host.
IXP_MANAGER_API_KEY yes Sent as X-IXP-Manager-API-Key; unlocks contact emails in the IX‑F export.
PEERINGDB_API_KEY yes Sent as Authorization: Api-Key <key>.
PEERINGDB_IX_ID 4727 no PeeringDB IX id to reconcile against.
MAILGUN_API_KEY yes HTTP Basic api:<key>.
MAILGUN_DOMAIN example.org no Mailgun sending domain.
MAILGUN_BASE_URL https://api.mailgun.net no US region. EU region: https://api.eu.mailgun.net.
MAIL_FROM OpenIX <openix-noreply@example.org> no From header.
MAIL_REPLY_TO OpenIX NOC <noc@example.org> no Set as h:Reply-To.
MAIL_CC noc@example.org no CC on prod sends only; suppressed in test mode. Empty disables.
EXCLUDED_ASNS 4969,8097,212232,401714,399728,400786 no ASNs to never contact (route servers, collectors, IX/infra ASNs).
OVERRIDES_PATH ./overrides.json no Optional ASN→email override file (see below).
DB_PATH ./data/oixPdbReminder.db no SQLite database path (WAL mode).
REMINDER_INTERVAL_DAYS 7 no Minimum age before a contacted member is due for a reminder.
SEND_THROTTLE_SECONDS 15 no Pause between each email (Mailgun rate-limit pacing; also paces after failures).

Per-ASN email overrides (optional)

If a network's IX‑F contact email is wrong or missing, you can override it. Copy overrides.example.json to overrides.json (or wherever OVERRIDES_PATH points) and map ASNs to one or more addresses:

{
  "64500": ["peering@example.net"],
  "64501": ["noc@example.com", "ipeng@example.com"]
}

Overrides take precedence over the IX‑F contact email.

Test vs. prod safety (the send blocker)

OIX_PEERING_DB_REMINDER gates real delivery — this is the primary safety mechanism:

  • prod → emails go to each member's real contact address (and CC MAIL_CC, if set).
  • anything else / unset → test modeevery message is redirected to TEST_RECIPIENT. The subject is prefixed [TEST AS<n> -> <intended>], and the originally intended address is still written to the audit log. MAIL_CC is suppressed.

Always start in test mode. Switch to prod only after reviewing a dry run.

Usage

bun run start                       # initial run: reconcile + email 'pending' members
bun run start --dry-run             # reconcile + print the report ONLY (no DB writes, no emails)
bun run start --reminders           # email members contacted >= REMINDER_INTERVAL_DAYS ago
bun run start --asn 64500           # target ONE network (great for testing a single email)
bun run start --dry-run --asn 64500 # preview just that one network in the report
bun run start --help
bun run typecheck                   # tsc --noEmit (type-check only)
bun run dev                         # run with --watch

Flags

  • --dry-run — Compute and print the missing-member report (counts + per-ASN table). No database writes, no emails. Use this to review who would be contacted.
  • --reminders — Send follow-up reminders to members already contacted at least REMINDER_INTERVAL_DAYS ago. Without this flag, a run sends the initial email to newly found pending members only.
  • --asn <list> — Restrict the run to specific ASNs (comma-separated; AS prefix optional, e.g. --asn AS64500,64501). Selects by ASN regardless of prior contact status, so it can be re-run repeatedly. Ideal for testing a single network. Still subject to the send blocker.
  • --help, -h — Show usage.

--asn and campaign state: A --asn send while not in prod is a preview send — it is written to send_log (tagged mode=test) but does not advance the member to contacted, so that network still receives its normal email in the real campaign run. In prod mode, --asn behaves as a real targeted send and tracks state normally.

Going live

  1. bun run start --dry-run — review the recipient list.
  2. Optionally test a single network: bun run start --asn <one-asn> (still test mode → goes to TEST_RECIPIENT).
  3. Set OIX_PEERING_DB_REMINDER=prod and run bun run start for real.

Scheduling reminders

The script is a one-shot — it has no internal scheduler. Drive the reminder cadence externally, e.g. a weekly cron that runs bun run start --reminders.

Project structure

src/
  index.ts                  # CLI entry: arg parsing, run flow, report + summary output
  config.ts                 # env loading + validation (zod), test/prod mode resolution
  reconcile.ts              # pure reconcile() + persistReconciliation() + selectSendBatch()
  db/
    schema.ts               # bun:sqlite init (WAL), table DDL
    reminders.ts            # ReminderRepository (member_state + send_log access)
  services/
    ixpManager.ts           # keyed IX-F export client (member contact emails + fabric details)
    peeringdb.ts            # PeeringDB client (who is listed + per-net flags)
    mailgun.ts              # Mailgun messages API client
    overrides.ts            # optional ASN→email[] override loader
  notifications/
    email.ts                # builds the per-member view-model
    templateRenderer.ts     # Handlebars renderer (escaping chosen by filename)
    sender.ts               # sendBatch(): render, redirect in test, send, log, record
  templates/
    email.subject.hbs       # subject line
    email.text.hbs          # plain-text body
    email.html.hbs          # HTML body
  types/
    domain.ts               # core domain types (statuses, member/send shapes)
    ixf.ts                  # IX-F export response types
    peeringdb.ts            # PeeringDB response types

Data sources

  1. Member contact emails — IXP Manager keyed IX‑F export (authoritative): GET {IXP_MANAGER_BASE_URL}/api/v4/member-export/ixf/1.0 with the API-key header. This keyed export includes per-member contact email plus each member's IPv4/IPv6 and port speed. (Public IX‑F copies typically strip contact data, so they are not used.)
  2. Who is already in PeeringDBGET /api/netixlan?ix_id=<PEERINGDB_IX_ID> for the set of listed ASNs, and GET /api/net?asn__in=<csv> for per-member flags (has_pdb_net, allow_ixp_update) that select the email variant.
  3. Mailgun messages API for delivery.

Database

SQLite (via bun:sqlite, WAL mode) at DB_PATH. It is the deduplication and reminder ledger — don't wipe or alter the schema/history without care.

  • member_state — one row per ASN: contact, IPs, speed, has_pdb_net, allow_ixp_update, status, send_count, first_contacted_at, last_contacted_at. Upserts preserve send tracking and never downgrade contacted/resolved back to pending.
  • send_log — append-only audit: one row per send attempt with intended_email, actual_email, type, mode, mailgun_id, status, error.

Member lifecycle status:

Status Meaning
pending Missing from PeeringDB, has a contact email, not yet emailed.
contacted Has been emailed; eligible for reminders after the interval.
no_contact Missing from PeeringDB but no contact email could be resolved (manual follow-up).
resolved Now listed in PeeringDB; dropped from future sends.

Email templates

Email content lives in Handlebars templates under src/templates/ — edit these to change wording, with no code changes required:

  • email.subject.hbs — subject line
  • email.text.hbs — plain-text body
  • email.html.hbs — HTML body

The renderer picks escaping by filename: *.html.hbs is HTML-escaped, while *.text.hbs and *.subject.hbs render with noEscape so quotes/ampersands stay literal in plain text. One email is sent per ASN with both text and HTML parts.

Available template variables include: isCreate, isReminder, networkName, asn, ixpName, ixpPeeringdbId, pdbBaseUrl, pdbIxUrl, ipv4, ipv6, ipv4Display, ipv6Display, speedLabel, ipv4Prefix, nocEmail, and year. Standalone {{#if ...}} lines are whitespace-stripped so conditional rows don't leave blank lines in the text body.

Branding for the HTML email (logo, footer text, links, addresses) is hardcoded in email.html.hbs / email.text.hbs — edit there. Templates are read from disk at runtime via import.meta.url (no build step). If the project is ever bundled, copy src/templates/ alongside the output.

Run flow

  1. Load and validate config (logs PROD vs TEST); load overrides.
  2. reconcile() → print the report (counts + per-ASN table).
  3. --dry-run stops here.
  4. Initialize the database → persist the reconciliation (upserts + marks resolved members).
  5. Select the send batch. Eligibility is the live missing set (the same source --dry-run prints), so excluded ASNs, members now listed in PeeringDB, and members that left the IX are never selected. The DB supplies only the dedup status (pending for an initial run, or contacted-before-cutoff for --reminders).
  6. Send: render, redirect in test mode, CC on prod sends, deliver via Mailgun, then log + record contact. Paces SEND_THROTTLE_SECONDS before every attempt after the first (so a failed or rate-limited send is also followed by a wait). Failures stay pending/contacted for the next run.
  7. Print a summary (attempted / sent / failed / skipped, plus the no_contact count).

Development notes

  • No build step. Bun runs the .ts sources directly. Imports use .js ESM specifiers, and Bun resolves ./x.js to a real x.js if one exists — so a stray tsc emit next to the sources would silently shadow your .ts. tsconfig.json sets "noEmit": true and .gitignore ignores src/**/*.js to prevent this. Never commit compiled .js into src/.
  • Type-check with bun run typecheck (never a bare tsc).

License

ISC.

About

OpenIX PeeringDB Reminders - Check PeeringDB periodically and send reminders for networks to update their record.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors