Tools

ICE pair ranker

/tools/ice-pair-ranker/

Paste both sides' ICE candidates, as SDP lines, a whole offer, candidate JSON or getStats(): the pairs ICE forms, the ones it drops, the order it tries them, and what to fix.

How it works

ICE pair ranker#

What it answers#

Two computers trying to reach each other each list the addresses they might be reached at, their candidates, and ICE tries them in pairs until one works. Given both lists, which pairs does ICE form, which does it drop, in what order does it try the rest, and is there anything in the lists that will stop the call connecting?

Paste the candidates however you have them: a=candidate lines, a whole offer or answer, the JSON of an RTCIceCandidate or a list of them, or the JSON of getStats(), which holds both sides in one paste.

How to use it: an example#

Say a call between two browsers takes a while to connect, and you want to see what ICE had to work with. In the browser console of one side, run:

JavaScript
copy(JSON.stringify([...(await pc.getStats()).values()]))

1. Paste it into Local candidates, leave Remote candidates empty, and press Run. The report holds the local candidates and the remote ones the other side sent. The “From getStats()” sample is a report like that.

2. Read the order. It comes first, because the answer is a list:

plaintext
Computed check order, highest priority first. Local side controls
# Local Remote Route
1 host 7f3c1b2e-...local:50004 host 3b9e2f10-...local:61000 direct
2 host 7f3c1b2e-...local:50004 srflx 198.51.100.77:40100 direct
3 srflx 192.0.2.10:54321 host 3b9e2f10-...local:61000 direct
4 srflx 192.0.2.10:54321 srflx 198.51.100.77:40100 direct
5 host 7f3c1b2e-...local:50004 relay 198.51.100.51:49200 TURN
...
9 relay 198.51.100.50:49152 relay 198.51.100.51:49200 TURN

Each row also has its pair priority, the number the order is sorted by. In plain words: each side has three candidates, so there are nine pairs, and none is dropped. The first check is between the two machines’ own addresses, the last five go through a TURN server. It is the order RFC 8445 computes, not one this tool watched happen.

3. Read what to look at.

SeverityWhat we foundWhat it means and what to do
Note1 remote host candidate is an mDNS name, ending in .local.A name like that only resolves on the same local network. Across the internet, the reflexive or relayed pairs carry the call.
Note1 local reflexive candidate does not say which host address it was learned from.Browsers hide it as raddr 0.0.0.0. Its pairs are kept here; if the host it came from is listed, the browser prunes them.

The first row matters most. The first check, host to host, goes to a .local name, which only means something on the other machine’s own network. Across the internet that pair fails, and the call is carried by a server-reflexive or relayed pair further down the list. A warning, such as no relay on either side behind two NATs, comes straight after the order instead of here.

4. Read the grid for the shape. When both sides have two candidates or more, the grid puts local candidates down the side and remote ones across the top, and each cell is the pair’s place in the order: 1 2 5 for the host row, 3 4 7 for the srflx row, 6 8 9 for the relay row. The fields at the end give each candidate’s priority, decoded, and the formula that prices a pair.

Compare with the post’s call. Press “Two browsers”, where the same three-and-three are typed as lines with their real addresses. Now the srflx candidate names its base, raddr 192.168.1.42 rport 50004, so its three pairs are recognised as copies of the host’s and dropped: 6 checks instead of 9. The pruned table under the order lists the three, each with the address it is really sent from, 192.168.1.42:50004, and the check it duplicates.

How it works#

ICE never checks every candidate against every other in any order. RFC 8445 builds a checklist in four steps, and this tool shows each one:

  1. Pair. Every local candidate with every remote one that has the same component, the same transport and the same address family. An mDNS name, the .local address a browser shows instead of its private IP, pairs with either family, since only resolving it would say which. TCP candidates pair active with passive.
  2. Price. Each pair gets 2^32 * min(G, D) + 2 * max(G, D) + (G > D ? 1 : 0), where G is the controlling agent’s candidate priority and D the controlled one’s. So the same two candidates can rank differently depending on which side controls, by exactly the last term.
  3. Prune. A server-reflexive or peer-reflexive local candidate is replaced by its base, the host address its packets really leave from. A pair that then duplicates a higher-priority pair is dropped, because it would send the same check. This is why a server-reflexive candidate is never checked on its own: its job is to tell the other side where to send.
  4. Limit. Past 100 pairs, the default in section 6.1.2.5, the lowest are dropped.

What is left, highest priority first, is the order the checks run in.

What to look at lists what in the candidates can stop or slow a call: no relayed candidate on either side, so no fallback when UDP is blocked; an ICE-lite side set as controlling, which RFC 8445 forbids; type preferences that put a relay ahead of a direct path; more than 100 pairs; mDNS names; hidden bases; candidates with nothing to pair with; and the same candidate pasted twice.

What it reads, and what it does not#

  • Priorities are taken as given, not recomputed: they are what the two agents exchanged. Each is also broken into its type preference, local preference and component, so a line whose priority does not match its type shows.
  • The base of a reflexive candidate is found from its raddr and rport. A browser often hides those (raddr 0.0.0.0 rport 0); then the base cannot be found and the pair is kept, which is the cautious reading.
  • getStats() has no component, because a bundled call with rtcp-mux has only one, so its candidates are component 1.
  • Not read: chrome://webrtc-internals’ own dump format, which stores each statistic as a time series rather than as one report. Copy getStats() from the console instead.
  • Not modelled: which checks succeed. That depends on the networks in between, and no list of candidates says. Nor the frozen and waiting states, or triggered checks, which change when checks run but not their priority order.

References#

Source and references

index.ts496 lines, TypeScript
TypeScript
/**
* An ICE pair ranker: paste both sides' candidates and see the checklist RFC 8445 builds from them, in order.
*
* Three steps, each one a place to go wrong by hand:
*
* 1. **Pair** every local candidate with every remote one of the same component, transport and address family
* (section 6.1.2.2), and price each pair: 2^32*min(G,D) + 2*max(G,D) + (G>D?1:0), with G the controlling agent's
* candidate priority and D the controlled one's (6.1.2.3).
* 2. **Replace** each server-reflexive or peer-reflexive local candidate by its base, the address its packets really
* leave from, and **prune** a pair that now duplicates a higher-priority one (6.1.2.4). A server-reflexive
* candidate is never checked on its own; its job is to tell the other side where to send.
* 3. **Order** what is left by priority, and keep the first 100 (6.1.2.5). That is the order the checks run in.
*
* Candidates come from whatever the reader has: SDP `a=candidate` lines, a whole offer or answer, the JSON of an
* RTCIceCandidate or a list of them, or the JSON of `getStats()`, whose local-candidate and remote-candidate entries
* fill both sides at once. Copying the lines out by hand is the step people get wrong, so the tool does it.
*
* Priorities are read as given, never recomputed, because they are what the agents exchanged; each one is also
* decoded into its type, local preference and component, so a hand-made line that does not match its own type is
* visible.
*/
import type { Cell, Field, Output, Tool } from "@toolbench/sdk";
type Input = { local: string; remote: string; controlling: string };
class InputError extends Error {
readonly input: string;
constructor(message: string, input: string) {
super(message);
this.input = input;
}
}
interface Candidate {
where: string;
foundation: string;
component: number;
transport: string;
priority: number;
address: string;
port: number;
type: string;
raddr?: string;
rport?: number;
tcptype?: string;
}
const TYPE_PREF: Record<number, string> = { 126: "host", 110: "prflx", 100: "srflx", 0: "relay" };
const TYPE_NAME: Record<string, string> = { host: "host", srflx: "server-reflexive", prflx: "peer-reflexive", relay: "relayed" };
/** RFC 8445 section 5.1.2.2's recommended order. Only the order is a rule; the numbers are recommendations. */
const TYPE_RANK: Record<string, number> = { host: 3, prflx: 2, srflx: 1, relay: 0 };
/** The default checklist size, RFC 8445 section 6.1.2.5. */
const PAIR_LIMIT = 100;
/**
* One `candidate:` attribute, as RFC 8839 section 5.1 writes it, at a known position. Each token stops at a quote,
* a backslash or a comma, so the same expression reads a line of SDP and a candidate string inside JSON.
*/
const CANDIDATE =
/candidate:([^\s"\\,]+)\s+(\d+)\s+([^\s"\\,]+)\s+(\d+)\s+([^\s"\\,]+)\s+(\d+)\s+typ\s+([^\s"\\,]+)((?:\s+[a-z][a-z-]*\s+[^\s"\\,]+)*)/iy;
/** Where a candidate attribute starts: "candidate:" not inside a longer word, so not "remote-candidates:". */
const START = /(?<![\w-])candidate:/gi;
interface Parsed {
candidates: Candidate[];
/** getStats' remote-candidate entries, which fill the other side when it was left empty. */
others: Candidate[];
repeats: number;
lite: boolean;
source: "lines" | "stats";
}
function fromLines(text: string, input: string): Candidate[] {
const out: Candidate[] = [];
for (const start of text.matchAll(START)) {
CANDIDATE.lastIndex = start.index;
const m = CANDIDATE.exec(text);
const line = text.slice(0, start.index).split("\n").length;
if (!m) {
throw new InputError(
`Line ${line} has a candidate that does not parse: expected "candidate:<foundation> <component> <transport> <priority> <address> <port> typ <type>".`,
input,
);
}
const extras = m[8] ?? "";
const raddr = /\braddr\s+([^\s"\\,]+)/.exec(extras)?.[1];
const rport = /\brport\s+(\d+)/.exec(extras)?.[1];
const tcptype = /\btcptype\s+(\w+)/.exec(extras)?.[1];
const type = m[7]!.toLowerCase();
if (!TYPE_NAME[type]) throw new InputError(`Line ${line} has type "${m[7]}", but ICE's types are host, srflx, prflx and relay.`, input);
out.push({
where: `line ${line}`,
foundation: m[1]!,
component: Number(m[2]),
transport: m[3]!.toLowerCase(),
priority: Number(m[4]),
address: m[5]!,
port: Number(m[6]),
type,
...(raddr ? { raddr } : {}),
...(rport ? { rport: Number(rport) } : {}),
...(tcptype ? { tcptype: tcptype.toLowerCase() } : {}),
});
}
return out;
}
type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
/**
* getStats' candidate entries (W3C webrtc-stats, RTCIceCandidateStats), from `JSON.stringify([...report.values()])`,
* an object keyed by id, or `[...report]` pairs: every object anywhere in the paste whose `type` is local-candidate or
* remote-candidate. getStats has no component, because a bundled, rtcp-muxed call has only one, so it is 1.
*/
function fromStats(text: string): { local: Candidate[]; remote: Candidate[] } | undefined {
const trimmed = text.trim();
if (!/^[[{]/.test(trimmed)) return undefined;
let json: Json;
try {
json = JSON.parse(trimmed) as Json;
} catch {
return undefined;
}
const local: Candidate[] = [];
const remote: Candidate[] = [];
const walk = (v: Json): void => {
if (Array.isArray(v)) return v.forEach(walk);
if (v === null || typeof v !== "object") return;
const t = v.type;
if (t === "local-candidate" || t === "remote-candidate") {
const side = t === "local-candidate" ? local : remote;
const address = String(v.address ?? v.ip ?? "");
const type = String(v.candidateType ?? "").toLowerCase();
if (address && TYPE_NAME[type] && typeof v.port === "number" && typeof v.priority === "number") {
side.push({
where: `entry ${String(v.id ?? side.length + 1)}`,
foundation: String(v.foundation ?? v.id ?? side.length + 1),
component: 1,
transport: String(v.protocol ?? "udp").toLowerCase(),
priority: v.priority,
address,
port: v.port,
type,
...(typeof v.relatedAddress === "string" && v.relatedAddress ? { raddr: v.relatedAddress } : {}),
...(typeof v.relatedPort === "number" ? { rport: v.relatedPort } : {}),
...(typeof v.tcpType === "string" ? { tcptype: v.tcpType } : {}),
});
}
return;
}
Object.values(v).forEach(walk);
};
walk(json);
return local.length || remote.length ? { local, remote } : undefined;
}
/** The same candidate pasted twice, from two m-sections of an unbundled SDP or two copies of a line. */
function dedupe(list: Candidate[]): { kept: Candidate[]; repeats: number } {
const seen = new Set<string>();
const kept = list.filter((c) => {
const key = `${c.component}|${c.transport}|${c.address}|${c.port}|${c.type}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
return { kept, repeats: list.length - kept.length };
}
function parse(text: string, input: string): Parsed {
const lite = /(?:^|\n)\s*a=ice-lite\b/.test(text);
const stats = fromStats(text);
if (stats) {
const a = dedupe(stats.local);
const b = dedupe(stats.remote);
return { candidates: a.kept, others: b.kept, repeats: a.repeats + b.repeats, lite, source: "stats" };
}
const { kept, repeats } = dedupe(fromLines(text, input));
return { candidates: kept, others: [], repeats, lite, source: "lines" };
}
/** IPv4, IPv6, or an mDNS name, which a browser uses to hide a private address and which pairs with either. */
function family(address: string): "v4" | "v6" | "mdns" {
if (/\.local\.?$/i.test(address)) return "mdns";
return address.includes(":") ? "v6" : "v4";
}
/** An address the internet cannot route to: RFC 1918, carrier-grade NAT, link-local, unique-local IPv6. */
function isPrivate(address: string): boolean {
if (family(address) === "mdns") return true;
const v4 = /^(\d+)\.(\d+)\./.exec(address);
if (v4) {
const [a, b] = [Number(v4[1]), Number(v4[2])];
return a === 10 || a === 127 || (a === 172 && b >= 16 && b < 32) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b < 128) || (a === 169 && b === 254);
}
return /^(?:f[cd]|fe[89ab])/i.test(address) || address === "::1";
}
const pairPriority = (g: number, d: number) => 2n ** 32n * BigInt(Math.min(g, d)) + 2n * BigInt(Math.max(g, d)) + (g > d ? 1n : 0n);
/** The base a check is sent from: a reflexive candidate's related address, which is the host it was learned from. */
function base(c: Candidate, locals: Candidate[]): Candidate {
if (c.type !== "srflx" && c.type !== "prflx") return c;
return locals.find((l) => l.type === "host" && l.address === c.raddr && l.port === c.rport && l.component === c.component) ?? c;
}
/*
* A reflexive candidate whose related address names no listed host. Browsers send "raddr 0.0.0.0" to hide it, and
* the check still leaves from a host, so calling the candidate its own base would teach the opposite of the RFC.
* It is not pruned either: the host may simply not be in the list, and then this pair is checked.
*/
const reflexive = (c: Candidate) => c.type === "srflx" || c.type === "prflx";
/** TCP candidates pair active with passive and simultaneous-open with itself (RFC 6544 section 6.2). */
function tcpCompatible(l: Candidate, r: Candidate): boolean {
if (l.transport !== "tcp" || !l.tcptype || !r.tcptype) return true;
if (l.tcptype === "so") return r.tcptype === "so";
return (l.tcptype === "active" && r.tcptype === "passive") || (l.tcptype === "passive" && r.tcptype === "active");
}
function decode(p: number): string {
const tp = Math.floor(p / 2 ** 24);
return `Type preference ${tp}${TYPE_PREF[tp] ? `, which is ${TYPE_PREF[tp]} in RFC 8445's recommended values` : ""}. Local preference ${(p >>> 8) & 0xffff}, component ${256 - (p & 0xff)}.`;
}
type Tone = "good" | "warn" | "bad";
type Action = { tone: Tone | "normal"; problem: string; fix: string };
/** A relay that outranks a direct path on its own side: its checks run first and its traffic goes through a server. */
function rankProblem(side: Candidate[], name: string): Action | undefined {
const tp = (c: Candidate) => Math.floor(c.priority / 2 ** 24);
for (const a of side) {
for (const b of side) {
if (TYPE_RANK[a.type]! < TYPE_RANK[b.type]! && tp(a) > tp(b)) {
return {
tone: "warn",
problem: `The ${name} ${a.type} candidate on ${a.where} has a higher type preference (${tp(a)}) than the ${b.type} on ${b.where} (${tp(b)}).`,
fix: `RFC 8445 asks for host above peer-reflexive above server-reflexive above relayed. With this order, checks through the ${a.type} candidate run first. If the stack was configured by hand, put the type preferences back in that order.`,
};
}
}
}
return undefined;
}
export default {
run(input: Input) {
try {
const mine = parse(input.local, "local");
const theirs = input.remote.trim() ? parse(input.remote, "remote") : undefined;
const locals = mine.candidates;
// One getStats paste has both sides; the remote box can then stay empty.
const remotes = theirs ? (theirs.source === "stats" && theirs.candidates.length === 0 ? theirs.others : theirs.candidates) : mine.others;
if (locals.length === 0) {
throw new InputError("No local candidates found. Paste a=candidate lines, a whole SDP, RTCIceCandidate JSON, or the JSON of getStats().", "local");
}
if (remotes.length === 0) {
throw new InputError(
mine.source === "stats"
? "The getStats paste has no remote-candidate entries. Paste the other side's candidates here."
: "No remote candidates found. Paste the other side's a=candidate lines, its SDP, or getStats JSON with remote-candidate entries.",
"remote",
);
}
const localControls = input.controlling !== "remote";
const repeats = mine.repeats + (theirs?.repeats ?? 0);
type Pair = { l: Candidate; r: Candidate; b: Candidate; priority: bigint; state: "check" | "pruned" | "over"; same?: Pair };
const pairs: Pair[] = [];
for (const l of locals) {
for (const r of remotes) {
if (l.component !== r.component || l.transport !== r.transport || !tcpCompatible(l, r)) continue;
const fl = family(l.address);
const fr = family(r.address);
if (fl !== "mdns" && fr !== "mdns" && fl !== fr) continue;
const [g, d] = localControls ? [l.priority, r.priority] : [r.priority, l.priority];
pairs.push({ l, r, b: base(l, locals), priority: pairPriority(g, d), state: "check" });
}
}
if (pairs.length === 0) {
throw new InputError("No candidates can be paired: a pair needs the same component, the same transport and the same address family on both sides.", "remote");
}
pairs.sort((a, b) => (a.priority > b.priority ? -1 : a.priority < b.priority ? 1 : 0));
const seen = new Map<string, Pair>();
let kept = 0;
for (const p of pairs) {
const key = `${p.b.address}:${p.b.port}|${p.r.address}:${p.r.port}|${p.l.component}`;
const earlier = seen.get(key);
if (earlier) {
p.state = "pruned";
p.same = earlier;
} else if (++kept > PAIR_LIMIT) {
p.state = "over";
} else seen.set(key, p);
}
const checks = pairs.filter((p) => p.state === "check");
const pruned = pairs.filter((p) => p.state === "pruned").length;
const over = pairs.filter((p) => p.state === "over").length;
const label = (c: Candidate) => `${c.type} ${c.address}:${c.port}`;
const words = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;
// What to do, worst first. Each is a rule a reader can act on, not a restatement of the table.
const actions: Action[] = [];
const remoteLite = theirs?.lite ?? false;
if (mine.lite && remoteLite) {
actions.push({ tone: "warn", problem: "Both sides are ICE-lite.", fix: "Between two lite agents no connectivity checks are sent at all (RFC 8445 section 6.1.1), so this order never runs. One side has to run full ICE." });
} else if (remoteLite && !localControls) {
actions.push({ tone: "warn", problem: "The remote side is ICE-lite, and a lite agent is always the controlled one.", fix: "Set the controlling side to Local (RFC 8445 section 6.1.1)." });
} else if (mine.lite && localControls) {
actions.push({ tone: "warn", problem: "The local side is ICE-lite, and a lite agent is always the controlled one.", fix: "Set the controlling side to Remote (RFC 8445 section 6.1.1)." });
}
const anyRelay = [...locals, ...remotes].some((c) => c.type === "relay");
if (!anyRelay) {
const publicRemote = remotes.some((c) => c.type === "host" && !isPrivate(c.address));
actions.push(
publicRemote
? {
tone: "normal",
problem: "Neither side has a relayed candidate, but the remote side has a public host address.",
fix: "That usually connects. Only a network that blocks UDP stops it; a TURN server over TCP or TLS on port 443 is the fallback.",
}
: {
tone: "warn",
problem: "Neither side has a relayed candidate, so there is no fallback through a server.",
fix: "If a firewall blocks UDP, or both NATs give a new port per destination, every check fails and the call never connects. Add a TURN server to iceServers.",
},
);
}
for (const problem of [rankProblem(locals, "local"), rankProblem(remotes, "remote")]) if (problem) actions.push(problem);
if (over) {
actions.push({
tone: "warn",
problem: `${words(pairs.length - pruned, "pair is", "pairs are")} left after pruning, more than the ${PAIR_LIMIT} a checklist holds by default.`,
fix: `The lowest ${words(over, "pair is", "pairs are")} never checked (RFC 8445 section 6.1.2.5). Gather fewer candidates, for example one network interface.`,
});
}
const mdnsRemote = remotes.filter((c) => family(c.address) === "mdns").length;
if (mdnsRemote) {
actions.push({
tone: "normal",
problem: `${words(mdnsRemote, "remote host candidate is an mDNS name", "remote host candidates are mDNS names")}, ending in .local.`,
fix: "A name like that only resolves on the same local network. Across the internet, the reflexive or relayed pairs carry the call.",
});
}
const hidden = locals.filter((c) => reflexive(c) && base(c, locals) === c).length;
if (hidden) {
actions.push({
tone: "normal",
problem: `${words(hidden, "local reflexive candidate does", "local reflexive candidates do")} not say which host address ${hidden === 1 ? "it was" : "they were"} learned from.`,
fix: "Browsers hide it as raddr 0.0.0.0. Its pairs are kept here; if the host it came from is listed, the browser prunes them.",
});
}
const unpaired = [...locals.map((c) => ["local", c] as const), ...remotes.map((c) => ["remote", c] as const)].filter(
([side, c]) => !pairs.some((p) => (side === "local" ? p.l : p.r) === c),
);
if (unpaired.length) {
actions.push({
tone: "normal",
problem: `${words(unpaired.length, "candidate has", "candidates have")} nothing to pair with: ${unpaired.map(([side, c]) => `${side} ${label(c)}`).join(", ")}.`,
fix: "A pair needs the same component, transport and address family on both sides. An IPv6 candidate needs an IPv6 one on the other side.",
});
}
if (repeats) {
actions.push({
tone: "normal",
problem: `${words(repeats, "candidate was", "candidates were")} pasted more than once.`,
fix: "Each is counted once. An SDP without BUNDLE repeats its candidates in every media section.",
});
}
const worst = { bad: 0, warn: 1, normal: 2, good: 3 };
actions.sort((a, b) => worst[a.tone] - worst[b.tone]);
/*
* The answer is a list, so it is drawn as one: the computed order, which the tool works out from the RFC and
* does not observe. The arithmetic and the candidates' own priorities go last, for the reader checking it.
*/
const method: Field[] = [
{
group: "Roles",
label: "Controlling",
value: localControls ? "local" : "remote",
note: "Its candidate priorities are G in the pair formula, the other side's D.",
},
{ group: "Roles", label: "Pair priority", value: "2^32 x min(G, D) + 2 x max(G, D) + (G > D ? 1 : 0)", note: "RFC 8445 section 6.1.2.3" },
{
group: "Roles",
label: "Read from",
value: mine.source === "stats" || theirs?.source === "stats" ? "getStats() JSON" : "candidate attributes",
note: mine.source === "stats" && !theirs ? "The local-candidate and remote-candidate entries of one report." : "Every candidate: attribute in the paste, wherever it was.",
},
...locals.map((c) => ({ group: "Local candidates", label: `${c.type}, ${c.where}`, value: String(c.priority), note: decode(c.priority) })),
...remotes.map((c) => ({ group: "Remote candidates", label: `${c.type}, ${c.where}`, value: String(c.priority), note: decode(c.priority) })),
];
const order = new Map(checks.map((p, i) => [p, i + 1]));
const via = (p: Pair) => (p.l.type === "relay" || p.r.type === "relay" ? "TURN" : "direct");
const ranked: Output = {
kind: "table",
role: "comparison",
id: "order",
caption: `Computed check order, highest priority first. ${localControls ? "Local" : "Remote"} side controls`,
columns: [
{ label: "#", align: "end", mono: true, wrap: "none", rowHeader: true },
{ label: "Local", mono: true, wrap: "none" },
{ label: "Remote", mono: true, wrap: "none" },
{ label: "Route", wrap: "prose" },
{ label: "Priority", align: "end", mono: true, wrap: "none" },
],
rows: checks.map((p): Cell[] => [
{ text: String(order.get(p)), mono: true },
{ text: label(p.l), mono: true },
{ text: label(p.r), mono: true },
via(p),
{ text: String(p.priority), mono: true, tone: "muted" },
]),
};
const dropped = pairs.filter((p) => p.state !== "check");
const prunedTable: Output | null = dropped.length
? {
kind: "table",
role: "decoded",
id: "pruned",
caption: over
? `Pruned, ${words(pruned, "pair", "pairs")} sending the same check as one above, and ${over} past the ${PAIR_LIMIT} a checklist holds`
: `Pruned: ${pruned === 1 ? "this pair sends" : "these pairs send"} the same check, from the same place to the same place, as one above`,
columns: [
{ label: "Local", mono: true, wrap: "none", rowHeader: true },
{ label: "Sent from", mono: true, wrap: "none" },
{ label: "Remote", mono: true, wrap: "none" },
{ label: "Same as", align: "end", wrap: "none" },
],
rows: dropped.map((p): Cell[] => [
{ text: label(p.l), mono: true },
{ text: p.b !== p.l ? `${p.b.address}:${p.b.port}` : reflexive(p.l) ? "a host, hidden" : `${p.l.address}:${p.l.port}`, mono: true },
{ text: label(p.r), mono: true },
p.same ? `#${order.get(p.same)}` : { text: `over ${PAIR_LIMIT}`, tone: "muted" },
]),
}
: null;
const severity = (t: Action["tone"]) => (t === "bad" ? "Problem" : t === "warn" ? "Warning" : "Note");
const findings: Output | null = actions.length
? {
kind: "table",
role: "findings",
id: "findings",
caption: "What to look at, most serious first",
columns: [
{ label: "Severity", wrap: "none" },
{ label: "What we found", wrap: "prose" },
{ label: "What it means and what to do", wrap: "prose" },
],
rows: actions.map((a): Cell[] => [{ text: severity(a.tone), tone: a.tone === "normal" ? "muted" : a.tone === "good" ? "muted" : a.tone }, a.problem, a.fix]),
}
: null;
/*
* The grid the post draws: local candidates down, remote across, each cell the pair's place in the check
* order. A rank is a quantity of "how far down the list", so the one-hue scale gives a later check more ink (the
* host decides which end that is in each theme) and the key's "1 runs first" says which way to read it; a pruned or
* impossible pair is an empty cell rather than a colour that would claim a place.
*
* Only when both sides have two candidates or more: with one on either side it is a single row or column, and a
* gradient along it says nothing the numbered list does not.
*/
const values = locals.map((l) =>
remotes.map((r) => {
const p = pairs.find((x) => x.l === l && x.r === r);
return p && p.state === "check" ? (order.get(p) ?? null) : null;
}),
);
const heatmap: Output | null =
locals.length >= 2 && remotes.length >= 2
? {
kind: "heatmap",
heatmap: {
label: "Check order (1 runs first)",
xLabel: "Remote",
yLabel: "Local",
x: remotes.map((c) => `${c.type} ${c.port}`),
y: locals.map((c) => `${c.type} ${c.port}`),
values,
scale: "sequential",
},
}
: null;
const urgent = actions.some((a) => a.tone === "warn" || a.tone === "bad");
const parts: Output[] = [ranked];
if (findings && urgent) parts.push(findings);
if (prunedTable) parts.push(prunedTable);
if (findings && !urgent) parts.push(findings);
if (heatmap) parts.push(heatmap);
parts.push({ kind: "fields", role: "method", id: "method", fields: method });
return { kind: "group", parts };
} catch (error) {
if (error instanceof InputError) return { kind: "error", message: error.message, input: error.input };
throw error;
}
},
} satisfies Tool<Input>;

Related posts

2 items
  • Sep 23, 2026 23 min read Networking & media

    How two computers find a path

    Alice's laptop doesn't know the address the internet sees it at, and the assistant can't reply to an address it was never told. How one STUN round trip finds it, why some networks need a TURN relay, and how ICE checks every path and picks one.

    Real-time voice, from the packet upnetworkingwebrtc
  • Networking & media

    How two computers find a path

    Alice's laptop doesn't know the address the internet sees it at, and the assistant can't reply to an address it was never told. How one STUN round trip finds it, why some networks need a TURN relay, and how ICE checks every path and picks one.

    networkingwebrtc
    Sep 2026
  • Series

    Real-time voice, from the packet up

    How a voice call actually works, one layer at a time.

    6 posts