Files
ferrix/slides/dns-lookup.js
Rayhaan JaufeerallyandClaude Fable 5.1 2910a2a800 slides: light theme, final edits, single-file bundle
As presented. The palette is light on request of the reviewer; code blocks
and byte dumps keep a dark ground so the byte colours still work. The OPEN
walk lights the hovered bytes as an inverted block and its table cells are
dark text on a tint of their group colour.

build.py inlines reveal.js, the stylesheets, scripts and Markdown sections
into slides/dist/hands-on-bgp.html for uploading.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 20:40:55 +02:00

141 lines
6.5 KiB
JavaScript

// Live lookups on slides.
//
// <form class="dns-lookup"> with <input name="domain"> and a .dns-result:
// asks Google Public DNS over HTTPS for the name's AAAA records
// (https://developers.google.com/speed/public-dns/docs/doh/json) and
// prints the answer the way dig would.
// <form class="ripe-lookup"> with <input name="query"> and a .dns-result:
// asks the RIPE database (https://apps.db.ripe.net/docs/) what it knows
// about an address, prefix or AS number, and prints the objects the way
// whois would.
(function () {
const TYPES = { 1: "A", 5: "CNAME", 28: "AAAA" };
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
async function resolve(name, type) {
const r = await fetch(`https://dns.google/resolve?name=${encodeURIComponent(name)}&type=${type}`, {
headers: { accept: "application/dns-json" },
});
if (!r.ok) throw new Error(`dns.google answered HTTP ${r.status}`);
return r.json();
}
const line = (rr) =>
`<span class="name">${esc(rr.name)}</span> <span class="ttl">${rr.TTL}</span> IN <span class="type">${esc(TYPES[rr.type] || rr.type)}</span> <span class="${rr.type === 28 ? "addr" : "data"}">${esc(rr.data)}</span>`;
async function lookup(form) {
const out = form.querySelector(".dns-result");
const name = form.elements.domain.value.trim().replace(/\.$/, "");
if (!name) return;
out.innerHTML = `<span class="dim">asking dns.google for ${esc(name)} AAAA ...</span>`;
try {
const v6 = await resolve(name, "AAAA");
if (v6.Status === 3) {
out.innerHTML = `<span class="bad">NXDOMAIN</span> <span class="dim">there is no such name</span>`;
return;
}
if (v6.Status !== 0) {
out.innerHTML = `<span class="bad">the resolver answered with status ${v6.Status}</span>`;
return;
}
const answers = v6.Answer || [];
const lines = answers.map(line);
if (!answers.some((rr) => rr.type === 28)) {
// No IPv6 address. Show that this is a real answer, not a failure,
// and what the name does have.
const v4 = await resolve(name, "A");
lines.push(`<span class="dim">no AAAA record: this name has no IPv6 address</span>`);
(v4.Answer || []).filter((rr) => rr.type === 1).forEach((rr) => lines.push(line(rr)));
}
out.innerHTML = lines.join("\n");
} catch (e) {
out.innerHTML = `<span class="bad">lookup failed:</span> ${esc(e.message)} <span class="dim">(is there a network?)</span>`;
}
}
// ---- RIPE database ----------------------------------------------------
// Objects as apps.db.ripe.net shows them: one card per object, one row per
// attribute, references in blue, rows the RIPE NCC manages shaded. The
// API does not say which rows those are, so they are the ones the website
// shades for an assignment: the key, org, status, source, sponsoring-org
// and the RIPE NCC's own maintainers.
const RIPE_MANAGED = new Set(["inetnum", "inet6num", "aut-num", "org", "status", "source", "sponsoring-org"]);
const managed = (a) => RIPE_MANAGED.has(a.name) || (a.name === "mnt-by" && /^RIPE-NCC-/.test(a.value));
async function ripe(form) {
const out = form.querySelector(".ripe-result");
const query = form.elements.query.value.trim();
if (!query) return;
out.innerHTML = `<div class="ripe-msg">Searching the RIPE database for ${esc(query)} ...</div>`;
try {
const url = `https://rest.db.ripe.net/search.json?query-string=${encodeURIComponent(query)}&flags=no-referenced&flags=no-irt&source=ripe`;
const r = await fetch(url, { headers: { accept: "application/json" } });
if (r.status === 404) {
out.innerHTML = `<div class="ripe-msg">No results found for ${esc(query)}.</div>`;
return;
}
if (!r.ok) throw new Error(`rest.db.ripe.net answered HTTP ${r.status}`);
const objects = ((await r.json()).objects || {}).object || [];
out.innerHTML = objects
.map((o) => {
const rows = o.attributes.attribute.map((a) => {
const value = a.link || a.name === o.type ? `<span class="ref">${esc(a.value)}</span>` : esc(a.value);
return `<tr class="${managed(a) ? "managed" : ""}"><th>${esc(a.name)}:</th><td>${value}${a.comment ? `<span class="comment"># ${esc(a.comment)}</span>` : ""}</td></tr>`;
});
return `<div class="ripe-card"><table>${rows.join("")}</table></div>`;
})
.join("");
out.scrollTop = 0;
} catch (e) {
out.innerHTML = `<div class="ripe-msg">Lookup failed: ${esc(e.message)}. Is there a network?</div>`;
}
}
// The answer changes the slide's height; have reveal centre it again.
const relayout = () => window.deck && window.deck.layout();
document.addEventListener("submit", async (e) => {
const form = e.target.closest && e.target.closest("form.dns-lookup, form.ripe-lookup");
if (!form) return;
e.preventDefault();
await (form.classList.contains("ripe-lookup") ? ripe(form) : lookup(form));
relayout();
});
})();
// Byte walks: hovering (or tabbing to) a field row lights its bytes in the
// dump above it, and hovering bytes in the dump lights their row and scrolls
// it into view, so the dump can be used as the index while presenting.
(function () {
const pair = (el) => {
const walk = el.closest(".bytewalk");
if (!walk) return [];
const i = el.dataset.i;
return [walk.querySelector(`.bw-dump span[data-i="${i}"]`), walk.querySelector(`.bw-row[data-i="${i}"]`)].filter(Boolean);
};
const target = (e) => e.target.closest && e.target.closest(".bw-row, .bw-dump span[data-i]");
const on = (e) => {
const el = target(e);
if (!el) return;
const both = pair(el);
both.forEach((x) => x.classList.add("on"));
if (el.matches(".bw-dump span")) {
// Scroll the box itself, and instantly: scrollIntoView would also
// move the slide, and Chrome drops smooth scrolls inside the
// transformed slide that reveal positions.
const row = both.find((x) => x.classList.contains("bw-row"));
const box = row && row.closest(".bw-rows");
if (box) box.scrollTop = row.offsetTop - box.clientHeight / 2 + row.offsetHeight / 2;
}
};
const off = (e) => {
const el = target(e);
if (el) pair(el).forEach((x) => x.classList.remove("on"));
};
document.addEventListener("mouseover", on);
document.addEventListener("focusin", on);
document.addEventListener("mouseout", off);
document.addEventListener("focusout", off);
})();