Skip to content

Good Service Planning ​

Written 2026-10-09 at the start of the good sprint series, as the order of work for good's engineering spike: every measurement built, proven against real networks, and reported plainly in the two places a person meets good. What comes after, the articles, the visual design, the choosing and ordering of what a user sees, sits outside this plan's milestones on purpose. The map comes first, then the two places and the rule between them, then ten milestones of roughly an evening each, every one opening with its tasks as boxes, and last the two stages that follow them. We check the boxes as the work lands, and once a milestone is finished we rewrite the prose beneath its boxes from the plan into an introduction to what now exists: the new parts, how they work, what they're for, how to use them, and what they tell us about a connection.

World Map ​

The finished system, every part labeled, with the milestone that brings each part in parentheses. Six places, and the roads between them. The user is whoever has the page open in a browser tab and the desktop client running with its Connection tab showing; while we build, that is us, the first user. Read this first, then watch the milestones load the parts in and connect them.

By name. Every domain and subdomain in the system, what runs at each, what it does, and where.

  • good.ftorrent.com is the page: a Vue app on Vite, static files, served from our main server by Caddy behind HAProxy. It runs in the user's browser tab, fetches the reflectors, asks the STUN servers, meets moon's peers, and shows its verdicts.
  • ip.ftorrent.com is the HTTP address reflector, go-ip, one static binary behind the proxy on our main server. It answers one line: the address the request came from, read from the last forwarded address.
  • ip4.ftorrent.com is the same reflector under a name with only an A record, so a browser is forced to reach it over IPv4, which nothing else in a browser can do.
  • ip6.ftorrent.com is the same reflector under a name with only a AAAA record, forcing IPv6 the same way.
  • stun.ftorrent.com is coturn in stun-only mode on UDP 3478, both families, on our main server, published by Docker DNAT so it sees the real source and counted by firewall rules. It answers a binding request with the address and port it saw it arrive from.
  • open.ftorrent.com is the tracker, Aquatic, over UDP, HTTP, and WebSocket, on our main server. It introduces peers, and the engine announces to it for any torrent that lists it, as the torrents ftorrent makes will; whether a torrent the user brings gets it too is the add-torrent story's to decide.
  • moon.ftorrent.com is the second vantage: a small paid server at another place on the internet, built from cloud-init, the moon to the main server's sun. It runs coturn for a second STUN answer; a Go service behind Caddy for a second reflector, UDP pokes and TCP dials back to whoever asks, and a signaling room; two NAT namespaces we shape, with a pion peer and a UDP peer behind each, to punch through; and a host firewall with counters.
  • ip4.moon.ftorrent.com and ip6.moon.ftorrent.com are moon's reflector under single-family names, one record each, so the page's cross-check can force each family exactly as it does against the main server.
  • stun.cloudflare.com is Cloudflare's public STUN server: the second STUN answer until moon exists, then the fallback.
  • ipv4only.arpa is a name defined to have only A records, at the root of the internet's naming. A AAAA answer for it reveals DNS64 and names the NAT64 prefix.
  • docs.ftorrent.com is the documentation site, where this article lives, on our main server. Not part of good.
  • ftorrent.com is the home page and the installers, on our main server. Not part of good.

The panel, the Rust commands, and the engine have no name of their own. They run on the user's machine, and the next section places them.

By place. The same parts again, laid out where they run.

The user's page. A browser tab navigated to good.ftorrent.com.

  • The browser, running the page: a Vue app on Vite, static files, no server of its own (Good2). It fetches the reflectors over HTTPS, asks STUN servers over UDP through the browser's ICE (Interactive Connectivity Establishment), meets moon's signaling room over HTTPS, and opens WebRTC data channels to moon's peers (Good8).

The user's panel. The desktop client, running with its Connection tab showing, and the machine beneath it.

  • The desktop client, ftorrent, with the panel as its Connection tab: ConnectionPage.vue and a module beside it holding the logic, a list of facts above and the running-diagnosis box below, with Copy under both (Good1).
  • The client's Rust commands, general and named by operation: net_get and net_local, which exist today and grow a family (Good1); net_stun (Good2); net_interfaces and net_resolve (Good3); dialog_open, the system's open box (Good4); net_natpmp on the natpmp crate, net_pcp_announce, four UPnP commands on igd-next, and a fifth that fetches the router's device description (Good5); net_punch (Good8); and process_output, which runs the platform's route, interface, and firewall tools and answers what they print, so the panel can read them (Good3, Good5).
  • The engine, Python on libtorrent, one loop and no threads: session, poll, stats, tables, read, add, and stop (Good4). It listens on both families, joins the DHT, announces to the torrent's trackers, and hands every alert and counter to the panel.
  • The machine's own parts the panel reads: its interfaces and their addresses, its routing table and gateway, its system resolver, and its firewall, through socketfilterfw on macOS and the firewall's COM interface on Windows (Good3, Good5, changed with the user's say-so in Good9).

The user's network.

  • The router: NAT and a firewall over IPv4, a firewall alone over IPv6. It answers NAT-PMP and PCP on port 5351 and UPnP over SSDP and SOAP (read in Good5, asked for mappings and pinholes in Good9).
  • Sometimes a carrier's translator above the router, and sometimes DNS64 with a NAT64 translator; the panel detects both (Good3, Good5).

Our main server, under ftorrent.com.

  • good.ftorrent.com: the page's static files, served by Caddy behind HAProxy, uploaded by rsync.
  • ip.ftorrent.com, ip4.ftorrent.com, ip6.ftorrent.com: the HTTP address reflector, go-ip, one static binary behind the proxy, reading the last forwarded address. ip4 has only an A record and ip6 only a AAAA, which is how a browser is made to use one family.
  • stun.ftorrent.com: coturn in stun-only mode on UDP 3478, both families, published by Docker DNAT so it sees the real source. Counted by firewall rules from Good6 and guarded by the same rule with a drop target once the counts say what normal looks like.
  • open.ftorrent.com: the tracker, Aquatic. The engine announces to it for a torrent that lists it; Good4's session joins a torrent the person chooses, and announces to that torrent's own trackers.

moon, at moon.ftorrent.com. A small paid server at another place on the internet, entirely ours, built from cloud-init with its files in good/moon/ (Good6).

  • coturn in stun-only mode on 3478, both families: the second STUN vantage (Good6, used from Good7).
  • The Go service behind Caddy over HTTPS, which only ever sends to the address a request came from: the reflector, the UDP poke, and the TCP dial with a BitTorrent handshake (Good6), and the signaling room (Good8), all rate-limited per source. The reflector also answers at ip4.moon.ftorrent.com and ip6.moon.ftorrent.com, one record each, so a browser can be made to use one family.
  • Two network namespaces with source-NAT rules we write, one shaped like a home router and one symmetric (Good8). Behind each, a pion WebRTC peer for the page and a tiny UDP peer for the panel (Good8).
  • The host firewall, with the STUN counters and the dry-run guard (Good6).

Elsewhere.

  • stun.cloudflare.com: the second STUN vantage until moon exists, then the fallback (Good2).
  • The DHT and the swarm of a well-seeded torrent the person chooses: strangers who find the engine through the trackers and the DHT and dial in (Good4).
  • ipv4only.arpa: a name with only A records, whose AAAA answer reveals DNS64 (Good3).
  • Let's Encrypt, for moon's certificate, and the provider, for the box itself (Good6).

The roads.

  • From the page: HTTPS to the three reflectors and to moon's; UDP binding requests to stun.ftorrent.com, Cloudflare's, and moon's; HTTPS to moon's signaling room; WebRTC through the NATs to moon's two peers.
  • From the panel: the same HTTPS and STUN roads from sockets it binds itself; UDP to the router on 5351 and multicast to 1900; reads of the machine; HTTPS to moon for pokes, dials, and the punch exchange; UDP straight to moon's UDP peers through the NATs; commands to the engine.
  • From the engine: announces to a torrent's trackers, open.ftorrent.com among them when the torrent lists it, and to the DHT; listening sockets on both families that moon and strangers dial.
  • From moon, toward the user only: UDP pokes and TCP dials to the address a request came from.
the user                                              our main server, under ftorrent.com
  the page, in a browser tab ──────── HTTPS ─────────► good.          the page's files
  the panel, in the desktop client ── HTTPS ─────────► ip. ip4. ip6.  the reflector
     rust commands ────────────────── UDP ───────────► stun.          coturn
     the engine ───────────────────── TCP and UDP ───► open.          the tracker
         ▲ dialed by moon and by strangers
         │ through
  the router: NAT, firewall,                          moon, at moon.ftorrent.com
    NAT-PMP and PCP on 5351, UPnP ◄── reads, mappings   coturn          the second STUN
    (a carrier's NAT above it, sometimes)               Go service      reflector, poke, dial, signaling
                                                        two namespaces  cone and symmetric, each with
                                                                        a pion peer and a UDP peer
elsewhere: stun.cloudflare.com, the DHT and the swarm, ipv4only.arpa, Let's Encrypt, the provider

What comes out. The page's verdicts, the panel's facts and its box, and at the end of every run one self-describing document, each entry carrying its value, its method, and its vantage, labeled proven, inferred, or unknown (Good10).

The two places, and the rule between them ​

A person meets good in two places. The page is good.ftorrent.com: a browser, a sandbox, and whatever can be learned between that sandbox and our servers. The panel is the Connection tab in the desktop client: a list of facts at the top, a text box that fills the window, and Copy underneath, so whatever the panel finds can be pasted into a message or an issue. The panel reaches the machine through Rust commands and reaches BitTorrent through the engine, so it can do everything the page can and a great deal the page cannot.

They are separate code. The page is a Vue app on Vite in good/page/, scaffolded like open/page/ rather than on Nuxt, since one page with no routes wants the lighter scaffold, and built and uploaded the way the tracker's dashboard is; the panel is desktop/src/pages/ConnectionPage.vue with a module beside the client's other modules holding its logic. The two happen to use the same tools, so they will look alike and we carry ideas across freely, but no file, component, or package is shared between them, and nothing links the two builds. What they share is the servers they call and the contract of each server's answer. An earlier plan had one frontend serving both; it is withdrawn, and it is unlikely to come back. A desktop client built with web tools does not have to be the web page's code, and trying to make it so would make a mess of both. The deeper reason is that the two are opposites in what they ask of their web view: the page exists to use and test everything the browser's own connectivity allows, while the panel's web view is deliberately sandboxed, closed to foreign script, and reaches the network through Rust. And a browser-tier check run inside the client's web view would be testing the wrong browser, since what a user wants to know is whether their browser can be a peer, not whether WebKit inside ftorrent can.

The page stays plain static files with no server behind it, served by Caddy from the rsync target we own, so a new page goes up whenever we run the upload. The panel's Rust grows only general commands, named by operation and ignorant of good, per the lib.rs essay: a STUN binding to a server, the machine's interfaces, a name resolved, a packet sent to the gateway. The meaning of each answer is the panel's. The engine grows one dispatch entry per libtorrent call the panel needs, and the page polls it rather than the engine growing threads.

How the milestones are ordered, and why ​

The order follows two instincts. First, the free work comes first: everything that runs on infrastructure already live on our server and on the development machine, because those evenings cost nothing but time and each one teaches something the later ones depend on. Second, moon sits in the middle rather than at either end. Standing up a second server before the clients exist would mean guessing at its API; leaving it to the end would stall the three milestones that need a second address. So moon arrives once the page and the panel have run every check a single vantage allows and know exactly what they want from a second one.

Mutation comes after observation throughout. Nothing changes the router or the firewall until the panel can already measure what the change would do, so every change has a before and an after. Synthesis comes last, once every raw measurement exists to synthesize.

Each evening starts by reading the prior art named in its section, since established tools have already met most of these questions. The best single piece of prior art for the panel as a whole is Tailscale's netcheck, which reports UDP, each family, whether the mapping varies, hairpinning, and which port-mapping protocols the router speaks, all in one run; nothing we build should be worse than it at the things it does. For the page the nearest prior art is the browser NAT checkers that ask two STUN servers and compare. For moon's active tests it is Transmission's port test, a server at the project's domain that tries to connect back to the port the client reports.

Every evening ends the same way: smoke test on the development machine, and from a second internet connection whenever the result depends on the network, since one connection teaches one connection's lesson; note what was learned, correct anything the evening proved wrong, check the milestone's boxes here, and once the milestone is finished, rewrite its prose as an introduction to what now exists, that same evening. When an evening changes the page or a service, the public guide in good/README.md is re-read whole and corrected the same night, with its last-reviewed date moved; the first such evening is Good2, which also rewrites the guide's what's-next section against this file and retires its paragraph on the reserved pion slot.

Through Good10 the work gathers raw material, and condensing it comes after. Run every check every time, show every result as it came back, in the box where Copy takes it whole, spot check that each is present and correct, and move on. A verdict is a small reading laid over the results, never a substitute for one: no logic stands one server's answer in for another's or hides which check answered, since a little condensing done early masks what happened and muddles the real condensing at the end. A check that depends on an earlier one's answer is fine where the investigation needs that order. Keep each fact apart from its reading, so a reading can change later without measuring again. When an evening meets a new kind of connection, write its signature into the milestone's section in the three parts the later stage speaks in: the facts, the likely reading, and what it means for the person. When there's room for one more measurement, choose the one whose answer would change what a person is told or can do. And in Good9, keep each router's or firewall's exact answer, since each is a case the finished feature has to handle.

Good1: The page as it stands, and the panel's first facts ​

  • Good1a: Fetch ip4.ftorrent.com and ip6.ftorrent.com from the panel through net_get, time each, show both public addresses on the panel, and report a fetch that succeeds but takes seconds as present but unusable.
  • Good1b: Give net_local a family, so it binds a v6 socket toward a documentation address and answers the global v6 source address or that there is none.
  • Good1c: Make the text box the running diagnosis: a Run button, a header of date, version, and platform, one appended line per check with what it asked, what came back, and how long it took, and Copy taking the facts and the box together.

The page at good.ftorrent.com was still one static file through this milestone, as it had been since July: it fetched ip4.ftorrent.com and ip6.ftorrent.com at once and reported which families answered. The Connection tab in the desktop client asks the same questions and more, and it opens empty: nothing leaves the machine until the user presses Check.

A run asks two questions of each family, IPv4 and IPv6, and the table at the top sets the answers side by side: the address the machine sends from, and the address the internet sees.

The first question goes to the operating system. A machine holds several addresses at once: a private IPv4 address its router handed out, one IPv6 address or several, and a link-local address on every interface. For each connection, the system picks one as the source by looking up the route to the destination. The net_local command asks for that choice without sending anything: it connects a UDP socket toward an address beyond the local network, and connecting a UDP socket only settles its route and its source address, so the system answers and no packet leaves. The two addresses it connects toward, 192.0.2.1 and 2001:db8::1, are ones RFC 5737 and RFC 3849 set aside for documentation, so they name no one, and every address past the router gets the same answer, the default route's.

The second question goes to a server. The reflectors at ip4.ftorrent.com and ip6.ftorrent.com answer with the address the request arrived from, and each name has an address record in one family only, so a request to it can travel over that family and no other. The forcing lives in DNS, which is why the page and the panel can both rely on it: neither can tell its network stack which family to use, and neither can reach a name over a family the name has no address in.

Side by side, the two answers show how the families differ. On IPv4 they differ: the machine holds a private address like 192.168.1.23, and the router rewrites the source of every packet to the one public address its whole network shares, so a machine can learn its public IPv4 address only by asking something outside. On IPv6 they're usually the same address, since each device holds a globally routable address of its own and nothing translates it. The exception is the temporary address. Every consumer system makes a new IPv6 address for privacy about once a day and keeps the older ones for a while as deprecated, and in our measurement from a dual-stack connection, a Mac sent five of forty fetches from an address it had already deprecated, so the two IPv6 answers can differ with nothing translating between them.

The panel times each check, and we give each four seconds. A fetch over HTTPS is four round trips, one each for the name, the connection, the encryption, and the request, and eighty of them from a dual-stack connection took a median of 190 milliseconds. Four seconds leaves room for a satellite in geostationary orbit, near two and a half seconds a fetch, and for a lost packet sent again. So a check ends one of three ways: an answer, with its time; a failure, with its reason, like no route, which comes back in milliseconds; or no answer in four seconds, which only a path that silently drops what it's sent produces.

Beneath the table, the box keeps every run since the tab opened. Each run starts with when it ran, the client's version, and the platform, then a line per check with what it asked, how long it took, and what came back. Copy takes the table and the box together, so a whole report pastes into a message or an issue:

2026-10-17T16:00:00Z, ftorrent 0.1.0 on macOS
route to 192.0.2.1                5 ms  192.168.1.23
GET https://ip4.ftorrent.com/   245 ms  203.0.113.42
route to 2001:db8::1              5 ms  2001:db8:4a2c:100:8d3e:22f1:9b07:5c1e
GET https://ip6.ftorrent.com/   254 ms  2001:db8:4a2c:100:8d3e:22f1:9b07:5c1e

What these checks prove is that each family reaches a web server over TCP from this machine, and at which address the world sees it. They say nothing about UDP, which peer connections travel on, nothing about ports, and nothing about whether anyone outside can reach the machine; those are the questions the milestones after this one answer.

Good2: STUN from both places ​

  • Good2a: Move the page onto Vite and Vue in good/page/, mirroring open/page/, with an upload script that rsyncs the build to the directory Caddy serves.
  • Good2b: Gather server-reflexive candidates on the page from our STUN server: create the peer connection, open a data channel to start gathering, and show the mapped v4 address and port and whether a v6 candidate arrives.
  • Good2c: Compare on the page, per family, the address STUN saw with the address the HTTP reflector saw, and name what a disagreement means.
  • Good2d: Add two more gatherings, Cloudflare's STUN server alone and both servers together, and derive each family's mapping verdict from the reflexive candidates: the joint gathering's count against our server's alone, the same count endpoint-independent, double endpoint-dependent, and anything else unknown.
  • Good2e: Write net_stun in Rust: bind a UDP socket on a given port, send each of a list of servers a binding request built by hand, all at once, parse each response by hand, and answer for each server the mapped address, port, round trip, response size, or the error.
  • Good2f: Build the panel's mapping check: call net_stun once per family against both servers from one new local port, derive the mapping verdict, and show it with the HTTP-versus-STUN comparison on the panel.

Both places now learn their public address and port over UDP from stun.ftorrent.com, and both ask whether the router keeps one public port for every destination, the question hole punching turns on.

The page is a Vue app on Vite in good/page/, scaffolded like open/page/, and pnpm upload there builds it and ships it through upload.js at the repository root, the way every site goes up. On a computer it fills the window, with the facts and the verdict at the top and the box taking the rest of the height, and on a phone the facts stack, each name above its value, and the page scrolls. It runs as it opens: the two reflector fetches the page has made since July, and three WebRTC gatherings, all at once.

A page has no socket of its own, so it asks STUN the way the WebRTC project's Trickle ICE sample does, through the ICE dance, the steps by which two WebRTC peers find a path to each other. The dance's first step is gathering: the browser opens sockets on each of the machine's addresses, in both families, asks the STUN servers from each, and collects every address and port this end might be reached at as a candidate. The page makes a peer connection naming STUN servers and asks for a data channel, which is what sets the browser gathering, reads the candidates the browser hands back, and stops there, with no peer to send them to or test them against. A host candidate is a socket on this machine, and every mainstream browser hides its address behind a made-up name ending in .local, though not its port. A server-reflexive candidate is the public address and port a STUN server saw that socket's request arrive from, with its local address written 0.0.0.0 or :: for the same reason. The first gathering names our server alone, and its reflexive candidates fill the table's public address over UDP for each family. An IPv6 reflexive candidate moves the IPv6 claim from "reaches a web server" to "carries UDP," which is the claim a hole punch needs.

The table sets the UDP address beside the one the reflector saw over HTTPS. They should agree, IPv4 by its address and IPv6 by its /64, since a machine sends from more than one address in its network. When they don't, the row gives both addresses, and a sentence under the verdict says what most likely put them apart: over IPv4, a carrier's translator with more than one public address of its own, and in either family a VPN, a proxy, or a relay such as iCloud Private Relay carrying one path and not the other.

The second gathering names Cloudflare's server alone, which shows it answers from here, and the third names ours and Cloudflare's together, so the browser asks both from the same sockets. A router keys each mapping on the inside address and port, so when both servers see the same public port, their two candidates are identical, and the browser, which drops a candidate that repeats one it already has, hands back one, its line naming whichever server answered first. A browser gathers from as many sockets in a family as it likes, one per interface and, in Safari, two or three on one address, and never says how many. But asked by one server, each socket answers once, so the first gathering's count is the number of sockets. When the third gathering's count is the same, every socket kept one public port for both servers: endpoint-independent mapping, the kind a hole punch works through. When it's double, each socket got a new port for the second server: endpoint-dependent, the symmetric case, where it fails. Any other count, or a gathering that heard nothing, reads unknown. Cloudflare's server is the second answer until moon's takes its place in Good7.

The box keeps everything the browser reports, since at this stage what one browser writes and another leaves out is itself a finding. Under each check's summary line, with its host candidates counted, it lists every gathering in full: each candidate line as the browser wrote it, when it arrived, and the server it came from where the browser says; each error a STUN server's silence raised; and the browser's own record of each local candidate, from getStats. The ICE username fragment, the ufrag in every line, is a random label each peer connection makes for itself, so it tells which gathering a line belongs to even in a paste cut short. Chrome writes network-cost 999 on every candidate and networkType=unknown in getStats, declining to say whether a network is Wi-Fi or cellular, and Safari's getStats records carry fewer fields than Chrome's.

The panel asks the same questions with no browser in the way. net_stun takes a list of servers, a family, a local port, and a time limit, binds one UDP socket on that port, and sends every server a binding request at once, the way Tailscale's netcheck asks its servers, sending each again after half a second and then at doubling intervals until it answers or the time runs out. The request is twenty bytes built by hand, a header with STUN's magic cookie and a random transaction id, and the answer is parsed by hand, matched to its server by that id and read for its XOR-mapped address. That attribute can be decoded wrong and still yield a plausible address, so the test vectors STUN's authors published in RFC 5769 pin the decoding for both families. For each server the command answers the address and port it saw, the round trip, how many times it sent, and the size of the answer in bytes, which set against the request's twenty is the multiplier anyone reflecting traffic off a STUN server would get. Our server answers in 40 bytes over IPv4 and 52 over IPv6, and Cloudflare's in 32 and 44, coturn's answer carrying one more small attribute; with the IP and UDP headers every packet carries, ours comes to about 1.4 times the request on the wire over IPv4 and 1.5 over IPv6.

The panel calls net_stun once per family from a new port in the dynamic range each run, so a mapping the router made for the last run can't answer for this one, and the box shows a line for each server. The same public port at both servers is endpoint-independent mapping, with "port kept" beside it when that port is the one the machine chose, since then the router left it alone or nothing translates at all; different ports are endpoint-dependent. Asked from one socket it chose itself, with no counting of sockets, the panel's verdict is the authoritative form of the test. Its table compares our server's UDP address with HTTPS the same way the page's does. The socket is bound to no particular address, so over IPv6 the system picks the source for each packet, and on a Mac it sent STUN from the temporary address it had been using earlier that day while HTTPS went from the current one, the behavior Good1 found, which is why the comparison is of the /64.

Together the checks say whether each family carries UDP from here, at which public address and port, and whether the router keeps one port for every destination. They don't say whether a peer can reach this machine unasked, or how the router filters what arrives from someone it hasn't spoken to; those need a second vantage that sends to the machine, which is Good7's work, and the hole punch itself is Good8's.

We ran both places from a dual-stack connection, and the page in Safari from a phone on cellular data. On the dual-stack connection, both read endpoint-independent in both families, and its router kept the port the machine chose: the panel says so as "port kept," and the page shows it too, each IPv4 host candidate's port matching its reflexive one. On cellular, the carrier rewrites IPv4 ports, sends HTTPS and UDP out from two public IPv4 addresses in the same block, and still keeps one mapping per socket for every destination: in the third gathering every candidate named Cloudflare's server, ours having seen the same address and port and been dropped as a repeat, with no error to say ours went unheard. Over IPv6 on cellular, HTTPS arrived from a different network than UDP, from an address ending in ::8, the shape of a server's address rather than a phone's, so something carried Safari's HTTPS that didn't carry its WebRTC: a proxy the carrier runs, or a relay such as iCloud Private Relay, which from inside the page look the same.

Good3: What the machine knows about itself ​

  • Good3a: Write net_interfaces in Rust to list every interface with every address and its flags, through getifaddrs on a Mac and Linux and GetAdaptersAddresses on Windows, which also carries each adapter's gateways and each address's origin and lifetimes.
  • Good3b: Classify each address in the panel by the prefix table, and name the interface carrying the default route by matching the net_local answers against the list.
  • Good3c: Write process_output in Rust to run a program and answer what it printed, and read the gateway in each family through it from route on a Mac and ip route on Linux, with the parsing in the panel; Windows names its gateways through net_interfaces.
  • Good3d: Mark each v6 address stable, temporary, from DHCPv6, or deprecated, with its lifetimes, from ifconfig on a Mac and ip on Linux through process_output and from net_interfaces on Windows; name each interface as Settings does, from networksetup on a Mac; and say which of the machine's addresses the system sends from and the reflector saw.
  • Good3e: Write net_resolve in Rust to resolve a name through the system resolver and answer every address it returns.
  • Good3f: Resolve ipv4only.arpa in the panel to expose DNS64 and the NAT64 prefix, and resolve the ip4 and ip6 names to show the single-family records in the box.
  • Good3g: Add the shared-address-space check, counted only on the interface carrying the default route.
  • Good3h: Have net_stun also answer, for each server, the address its name resolved to, the address the answer came from, and the whole answer in hex.
  • Good3i: List beneath each STUN line in the panel's box where the request went, the address the system says it would send from by net_local toward that server, where the answer came from, the answer in hex, and each attribute in it by name, SOFTWARE as text.

The panel now reads the machine itself. Nothing here talks to a server beyond a name lookup; this is where the panel explains every address a person can find in their system's network settings, among them the ones that make a machine look as though it has IPv6 when it doesn't. The box ends with every interface the system has, up or down, and every address on each, and each line carries a few words on what it is.

net_interfaces lists them. A Mac and Linux answer through getifaddrs, which gives an entry for each address and one more for each interface's hardware, so an interface with no address still makes the list. A Mac shortens an IPv4 netmask to the bytes that aren't zero, so a /24 arrives in a record seven bytes long, and the command reads no further than the record says. Windows answers through GetAdaptersAddresses, which also carries each adapter's gateways and, for each address, where its prefix and its suffix came from and how long it lasts. A Mac lists close to twenty interfaces and uses one or two: the Wi-Fi or the Ethernet carrying the traffic, the loopback, several utun tunnels macOS keeps for its own services, awdl0 and llw0 for AirDrop and Continuity, and Ethernet and Thunderbolt ports with no link. The tunnels and the wireless links each hold an fe80 address of their own, valid only on their own link, and so does the interface carrying the traffic.

The panel sorts each address by the ranges IANA's special-purpose registries set aside. Over IPv4 that's private, shared, link-local, loopback, documentation, and public; over IPv6 it's link-local, unique local, Teredo, 6to4, NAT64's well-known prefix, documentation, and global. The interface carrying the default route is the one holding the address net_local says the system sends from. Rust has no portable way to ask for the gateway, so the panel runs route -n get default on a Mac and ip route show default on Linux in each family, through process_output, a general command that runs a program and answers what it printed, and reads the answer. Windows names its gateways through net_interfaces, which matters there, since its own tools print in the system's language.

Over IPv6 a machine holds several global addresses at once, and the panel says what each one is: a stable one made from the network's prefix and a secret; a temporary one made fresh for privacy, which new connections leave from; older temporaries kept as deprecated for connections already open; and, where the network runs DHCPv6, one handed out by its server. Beside each go how long it stays preferred for new connections and how long it stays valid. On a Mac those come from ifconfig -L, on Linux from ip -o address, and on Windows from net_interfaces, and a Mac's networksetup gives each interface the name Settings shows, like Wi-Fi. Windows tells its addresses apart by where each one's suffix came from: random for a temporary address, and link-layer for the stable one, though by default Windows makes the stable suffix up at random too, so there link-layer means stable rather than built from the hardware address. It also gives a temporary address a /128 where the stable one has a /64. On a dual-stack connection whose router advertised its prefix with lifetimes of five minutes, renewed with each advertisement, the stable and current temporary addresses showed about four minutes left while the DHCPv6 address showed days.

The panel marks the address the system says it sends from and the one the reflector saw, and the two can differ. In one run on a Mac, the HTTPS fetch and both STUN requests over IPv6 left from a temporary address deprecated hours before, while net_local, asked a moment later toward the same servers, named the current one. We haven't found why. The panel says what each source reported and leaves it there, which is why its STUN lines give the address the system says it would send from rather than claim where a request went.

net_resolve asks the system's resolver for every address a name has. ipv4only.arpa has only IPv4 addresses by definition, RFC 7050, so an IPv6 answer for it means the network makes up IPv6 addresses for names that have none, DNS64, for a translator to carry, NAT64, and where the IPv4 address sits inside that answer, at one of the six places RFC 6052 allows, names the translator's prefix. That turns a known false positive, an IPv6-only phone network passing the IPv4 check through its carrier's translator, into a labeled result. The panel also resolves ip4.ftorrent.com and ip6.ftorrent.com and shows the single-family records the reflector checks rest on.

An IPv4 address in the shared address space, 100.64.0.0/10, RFC 6598, on the interface carrying the default route says a carrier translates above the router. On any other interface it belongs to a mesh VPN, which borrows the range for its virtual interfaces, and the people running those are exactly our users.

Under each STUN line the box shows everything net_stun saw of that server: the address its name led to, the address the system says it would send from, the address the answer came from, the whole answer in hex, and each attribute in it by name, SOFTWARE as text. Our server answers with XOR-MAPPED-ADDRESS and FINGERPRINT, a checksum over the message, and Cloudflare's with XOR-MAPPED-ADDRESS alone, which is the eight bytes between their sizes. Over IPv4 the two servers' XOR-MAPPED values match byte for byte when the mapping is endpoint-independent, since the address is masked by the fixed cookie alone. Over IPv6 only the first eight bytes match, the family, the port, and the address's first four bytes, since the rest is masked by each request's own transaction id.

We built and ran this on a Mac and on Windows. If ip doesn't run inside the Flatpak's sandbox, Linux reads its address kinds and gateways as unknown.

Good4: libtorrent wakes up ​

  • Good4a: Give the engine a session command that creates lt.session from session_settings plus the diagnostic's choices: listen on every address of both families at one port, which the panel picks at random from the dynamic range at each Start, DHT on, UPnP, NAT-PMP, and local service discovery off, upload mode held for good, and the alert mask.
  • Good4b: Give the engine poll and stats commands: poll pops the alerts that arrived and emits each as an event with its type, message, and fields; stats asks for the session's counters and the DHT's routing table, which arrive as alerts on the next poll.
  • Good4c: Show on the panel the sockets the session opened, the DHT's bootstrap and its node count in all and in the largest routing table, every external address peers report, and every alert counted by kind, with the first of each kind in the box.
  • Good4d: Give the engine read and add commands and Rust a dialog_open command, and join from the panel the swarm of a torrent file chosen in the system's open box, in upload mode, counting incoming connections in each family by alert, with whether the session had sent to each address first, and by libtorrent's counter, with a Stop button that tears the session down.

The engine creates its first session, and the panel is the first place in the client that talks to it beyond hello. No download and no seed; the session exists to listen, to join the DHT, and to report what the network does to it.

Open… puts up the system's open box, and the engine reads the file chosen and answers its name, info-hashes, size, and files, so the table shows the path and the info-hash before anything joins. On a Mac dialog_open slides AppKit's open panel down from the window as a sheet; elsewhere it calls the dialog plugin's Rust half, whose JavaScript half the page is never granted. The choice lasts until the app quits. Start opens the session at a port picked at random from the dynamic range, 49152 to 65535, the range Transmission picks its random port from, a new one each time; a setting may later let a person fix one, with random staying the default. With a torrent chosen, Start also joins its swarm in upload mode, which announces and takes connections but requests nothing, so strangers find the session through the trackers and the DHT and dial in.

The session listens on 0.0.0.0 and [::] at that port, which libtorrent reads as every address the machine has, loopback and link-local included, opening a TCP socket and a UDP one on each. On a Mac that came to twenty addresses and forty sockets, nine of them global IPv6 addresses, most of those deprecated temporaries. From each address that reaches beyond the machine, ten there, libtorrent runs a DHT node with a routing table of its own, announces to the trackers, and keeps its own vote on the address peers see, which over IPv6 is just that address. Its DHT stats describe only one table, the one with the most buckets, so the panel asks each DHT node for its own and shows a row for each table: the node's ID, how many live nodes the table holds, and the address it runs from. Beside them is the dht.dht_nodes counter, which adds every table together, and which is also the number the dashboard on open.ftorrent.com reads from dht.ftorrent.com through qBittorrent. Whether the session that downloads wants every address is a choice for later.

UPnP and NAT-PMP stay off, so nothing asks the router for a mapping before Good5 has read it and Good9 has asked the user. Local service discovery is off too: with a socket on twenty addresses, the session heard its own announcement 939 times in one poll, dialed itself, and counted itself among the strangers. And upload mode is held. libtorrent takes an auto-managed torrent, which every torrent is by default, back out of upload mode after optimistic_disk_retry, ten minutes, in case the disk error it assumes put it there has cleared. A session left running did exactly that, downloaded the whole torrent into the temporary folder, and seeded it, and every session after found it there and seeded too. Adding the torrent unmanaged held it, but lost the start auto-management gives it, and with that the DHT lookups, which then waited for the fifteen-minute announce interval. So the torrent is added the usual way, and the session raises optimistic_disk_retry to sixty-eight years. The session that downloads puts all four back.

The panel polls once a second, for the alerts and, through stats, the counters. A session in a busy swarm posts a hundred alerts a second, most of them peers tried and dropped and DHT lookups, so the box shows the first alert of each kind, the table counts them all by kind, and the box gets the counts when the session stops. A connection in is the exception and gets a line every time, with a note on whether the session had sent to that address first, through a DHT lookup or a connection it tried. That's as far as the alerts show: the DHT's routine messages, its bootstrap queries and pings, post none.

On a dual-stack home connection behind a router with no port mapping, strangers connected in by the dozen, over both families, a hundred and ninety in twelve minutes in one run, and every one from outside came over uTP. The session tried hundreds of outgoing TCP connections in those runs, so other peers use TCP too, and none of theirs got in. uTP shares the session's UDP port with the DHT, so each DHT message the session sends opens the router for that node's replies, and a node that is also in the swarm connects in through the opening; nearly every connection in came from an address the session had sent to first. The few that hadn't, by the alerts, were the same few addresses session after session, some arriving within a second at a port that didn't exist a second before, which fits nodes the DHT's routine messages reached. So the zero this plan expected over IPv4 never came, and what the session shows is a router that admits answers on both families. Whether it admits a stranger who was never sent anything is the router's filtering, which Good7 measures with moon.

libtorrent's net.has_incoming_connections and its incoming_connection_alert disagree in one case, and the panel says so. libtorrent sets the counter as a connection arrives from beyond the local network, before deciding whether to keep it, and posts the alert only for one it keeps. A session with no torrent turns every connection away, so it reads yes by the counter with none by alert: a stranger turned away for having nothing to share.

This is also where the operating system notices the engine listening. The macOS firewall ships turned off, so on most Macs nothing appears. With it on, on macOS 15.7, the system asks whether ftorrent-engine, the engine rather than ftorrent, may accept incoming connections, and asks some seconds to over a minute after Start rather than at the first listen. Connections over uTP reached the engine before the prompt appeared and while it waited, and Allow changed nothing the counts could see. The allow list holds one entry for the engine's path, and socketfilterfw --listapps reads it without sudo. The answer outlasts a rebuild of unchanged code, whose ad-hoc signature comes out the same, but not a change to the engine, which brings the prompt back at the same path, so with the firewall on, an update that changes the engine would ask again. What triggers the prompt we haven't found. macOS 26 and 27 are checked on a Mac running them.

The Windows firewall ships turned on, and its rules follow a program's path, so an answer outlasts a rebuild or a reinstall in the same place. ftorrent itself had been allowed at both its paths, the installed copy and the development build, by earlier answers, so the Connection tab's Check brought no alert. The engine is a separate program at a path of its own, and its first listen at Start brought one, naming ftorrent-engine, about a second after Start. As the alert appeared, before anyone answered, Windows wrote two block rules for the engine, inbound TCP and UDP, on the Public profile, the profile of the network the machine was on; Allow access turned the same two rules into allows, on Public alone, and no UAC prompt followed. The alert offers the profiles as two checkboxes, Private unticked and Public ticked, the more permissive answer on a public network, though to someone who has never met Windows' network profiles it reads like a question about their own things: show what's already public, keep what's private private. Strangers didn't wait for the answer. The first connection in arrived over uTP 1.3 seconds after Start, while the block rules stood, from an address the session had sent to first, which a stateful firewall lets in as an answer. In a seventeen-minute session on a dual-stack home connection, 297 connections came in, 88 over IPv4 and 209 over IPv6: seven were the session dialing itself from another of its own addresses, one of those over TCP, every one from outside came over uTP, and all but twelve came from an address the session had sent to first. Upload mode held past libtorrent's ten minutes, with nothing downloaded. What Cancel leaves behind, and what a standard user without an administrator's password sees, are still to be seen. What each OS did and did not do is the ground Good5 stands on.

Good5: The firewall and the router, read-only ​

  • Good5a: Read the macOS firewall through socketfilterfw, run by process_output: the global state, block-all, stealth mode, the allowance for signed software, and the allow list, looking for ftorrent and the engine by path.
  • Good5b: Read the Windows firewall through its COM interface, INetFwPolicy2, from a Windows-only module with two commands, firewall_profiles and firewall_rules: each profile's state, the current network's profile, and every allow or block rule naming ftorrent or the engine by path; report unknown on Linux.
  • Good5c: Write net_natpmp in Rust on the natpmp crate: the public address request to the gateway on port 5351, answering the router's external address and epoch, or its result or the error.
  • Good5d: Write net_pcp_announce in Rust by hand, since no crate sends it: PCP's 24-byte ANNOUNCE to the gateway on port 5351 over IPv4, answering the router's result code and epoch, NAT-PMP's unsupported version from a router that speaks only that, or the error.
  • Good5e: Ask the router over UPnP through igd-next: write net_upnp_search, answering the gateway's address, its description and control addresses, its service type, and whether it offers IPv6 firewall control, and net_upnp_external, net_upnp_mappings, and net_upnp_firewall, reading its external address, every port mapping, and its IPv6 firewall's status from the gateway the page hands back.
  • Good5f: Compute the carrier-grade NAT likelihood from the router's external address against the reflector's, combined with the shared-address-space signal and the mapping verdict, with its reasons listed.

Observation only. The panel reports the standing of the machine's firewall and asks the router what it knows, and changes nothing.

On macOS the panel reads the firewall through socketfilterfw, five reads run through process_output, each answered without sudo on macOS 15.7 and 27 alike and each a line in the box with what it printed: the global state; block-all, which when on refuses every incoming connection whatever the list says; stealth mode, which leaves probes to closed ports unanswered; the allowance for built-in and for downloaded signed software; and the list of every program the firewall has been asked about, by path, allowed or blocked. ftorrent is signed ad hoc rather than with an Apple-issued identity, so the allowance for signed software covers neither it nor its engine, and whether each is admitted comes from the list. The panel looks there for this copy's executable, or the bundle holding it, since the list writes some programs as the .app, and for the engine's path, which the firewall asks about and lists on its own, so a development build's engine and an installed copy's are two entries. The table says whether the firewall is on and which of the two programs is listed, and the box, between the checks and the interfaces, shows the whole list with this copy's entries marked and the path looked for where there's no entry. The alf preferences the plan first named aren't there: /Library/Preferences/com.apple.alf.plist doesn't exist, and the file of that name in /usr/libexec/ApplicationFirewall/ is the factory template. One read misleads: --getappblocked answers permitted for any path at all, even one that doesn't exist, so it shows only an explicit block, and whether ftorrent is admitted comes from the allow list. The firewall is off by default on macOS, and the panel says so when it is, since an off firewall is not a problem.

On Windows the panel reads the firewall through its COM interface, INetFwPolicy2, in numbers rather than the words netsh prints in the system's language, through two commands in a Windows-only module, each a line in the box. firewall_profiles answers the three profiles, Domain, Private, and Public, and for each whether the current network is in it, whether the firewall is on for it, whether it refuses every incoming connection whatever the rules say, and what it does to a connection no rule names. firewall_rules takes a program's path and answers every rule naming it, with its name, direction, action, whether it's on, the profiles it applies in, its protocol, and its local ports; the panel asks twice, for this copy's executable and for its engine's. The rules come as a COM collection walked through its enumerator, each rule arriving in a VARIANT, so the walk reads the application name of every rule on the machine, 651 on this one, and keeps the few naming the path asked about, compared without regard to case, since the firewall stores the path the security alert gave it in lowercase. Each read takes a few tens of milliseconds and answers a standard user: the reads ran from a token with the Administrators group marked deny-only, the token every program gets under UAC until it's elevated, where PowerShell's Get-NetFirewallRule in the same shell answered access denied. The table says the current network's profile first, since Windows starts every new network as Public unless someone says otherwise, a home network included, and most people have never seen which theirs is; then the other two; then, in the row under it, each rule naming ftorrent or its engine in a few words, and the box lists the rules in full between the checks and the interfaces. A block rule counts as much as an allow, since it overrides one, and the security alert writes one for the current profile as it appears, so the panel lists both rather than looking only for an allow. Nothing here is particular to Windows 10 or 11: the interface has been the firewall's since Vista, and the panel reads both the same way.

On the Windows 10 machine, all three profiles were on, none blocked all incoming or allowed by default, and the current network, an Ethernet connection at home, was on Public. The rules naming ftorrent were the ones earlier security alerts had left, six in all, every one inbound, allow, enabled, and on Public alone, with a TCP rule and a UDP rule for each program and * for the local ports, the firewall's word for any, which the panel says as any ports. The installed copy's are named ftorrent.exe and its engine's ftorrent-engine.exe, after the file, and the development build's are named ftorrent, after the program's description, which is why the panel finds rules by path and never by name. The development build's engine had no rules at all, so the firewall has never been asked about it. A rule whose profiles are set to all comes back as all three names. The same firewall changes what the router's reads look like: the router refuses port 5351 with an ICMP port-unreachable, which a Mac's socket hears in milliseconds, but the Windows firewall's one inbound ICMPv4 rule on by default admits only the fragmentation-needed kind, so the refusal never reaches the socket, NAT-PMP and PCP both read no answer after their four seconds, and a Check that ends in milliseconds on the Mac takes four seconds here, since the table fills when the slowest check ends. On Windows, then, no answer on port 5351 is also what a refused port looks like. Adding an allow rule is Good9's business. Linux reports unknown, since ufw and nftables need root to read, and says so.

The router is asked three ways, all reads, once the first wave of checks has named the gateway, since two of the three go to that address; the UPnP reads follow the search that found the router, so a run is three waves where it was one. The protocols' mechanics stay low, from a crate where a good one exists, and libtorrent's two mappers can't serve here, since they look at the router only while they map. NAT-PMP's public address request is two bytes to the gateway on port 5351, sent through the natpmp crate by net_natpmp, which takes the gateway and answers the router's external address and its epoch, the seconds since its mapping table started, which libtorrent's own mapper reads and throws away. PCP has no request that reads the external address without making a mapping, but its ANNOUNCE asks for nothing and changes nothing: a router that speaks PCP answers it with a result code and its epoch, and one that speaks only NAT-PMP answers, in NAT-PMP's own form, that the version is unsupported, which is itself a finding. No crate sends it, so net_pcp_announce writes its 24 bytes by hand, over IPv4 for now, since the IPv6 gateway is a link-local address with an interface scope, which can wait for Good9's pinhole; the request carries the machine's own address, as PCP requires, so the command connects its socket to the gateway first and writes the source the system chose. UPnP is heavier: a multicast search to 239.255.255.250 on port 1900, the device description fetched over plain HTTP from the router, and SOAP calls to its WAN connection service. igd-next carries all of it, the crate iroh's port mapper and libp2p build their UPnP on, behind four commands: net_upnp_search, which answers the gateway's address, its description and control addresses, the actions its service offers, whether it offers the IPv6 firewall control service, and its service type, whose last number says which version of the profile the router speaks; and net_upnp_external, net_upnp_mappings, and net_upnp_firewall, which take that gateway back and read its external address, every mapping, walking the indexes until the router says the list has ended, since that walk is how UPnP lists mappings, and whether its IPv6 firewall is on and admits pinholes, where the router offers the service. igd-next's SOAP calls carry a time limit of their own, ten seconds, which nothing can shorten, so a read of a router that has stopped answering outlasts the panel's four seconds, and the panel says no answer first. The router's maker and model are in the device description the search read its control paths from, which igd-next keeps none of, so a fifth command, net_upnp_description, fetches it again, over plain http on the local network as UPnP serves it, and the panel reads the root device's manufacturer, model name and number, and friendly name into the UPnP row, in parentheses after the router's address, the way an interface's name follows an address; the home router here calls itself a Cisco DOCSIS gateway. Its uptime waits for the stage after this plan.

Two readings sit on the router's facts, each a row of its own. Which protocols the router speaks comes straight from what answered, with the firewall named when it may have held the search's answer back. And the carrier-grade NAT check that cannot be dodged: the router's own external address against the address the reflector saw. When they differ, a second translator sits above the home router, whatever address range the carrier chose; when that address is in the shared address space, 100.64.0.0/10, it's a carrier's, and a private one fits a second router, like a provider's modem, as well. Combined with the shared-address-space signal from Good3 and the mapping verdict from Good2, the panel gives a likelihood with its reasons listed, and beside an unlikely the note that address-plus-port sharing, where the carrier gives the router a public address and a slice of its ports, slips past all three.

On a dual-stack home connection, the router refused port 5351 outright, in a few milliseconds, so it speaks neither NAT-PMP nor PCP, and answered UPnP's search in about a second as version 1 of the profile, WANIPConnection:1, with nine actions, no IPv6 firewall control, no mappings, and an external address equal to the reflector's, so carrier-grade NAT read unlikely.

On a Mac, reaching the router passes two gates. The first is Local Network privacy, which Apple's technote TN3179 describes: the system asks the person the first time an app sends to a device on the local network or to a multicast address, except the network's DNS server, which on most home networks is the router itself, so UPnP's multicast search is what brings the question. Under the system's title goes one sentence from NSLocalNetworkUsageDescription in the Info.plist, plain on purpose, in the shape other apps use, since a person answers a box they recognize quickly: "ftorrent wants to access the local network." The system may refuse the first packet before the person answers, so a first check can fail where a second succeeds, and the privilege follows the code signature, which Apple tracks reliably only for an Apple-issued identity. A program run from a terminal takes the terminal's answer, so a development build says nothing about an installed copy. The installed copy's search went through with its grant already standing from an earlier run, so no box came that day, and the Local Network pane in System Settings lists ftorrent with its switch on, one entry for the app and the engine inside it, since that gate is per bundle where the firewall's list is per program. The second gate is the macOS firewall: with it on, a program not on its allow list doesn't hear the router's answer to a UPnP search, since the answer comes from the router's own address rather than the multicast address the search went to, so the firewall treats it as unasked and prompts. The first run on a Mac with the firewall on did exactly that: the prompt came up, the search waited its four seconds and heard nothing, and the run after Allow found the router. So a search that hears nothing while ftorrent isn't on the allow list reads as the firewall's doing, not as a router without UPnP, and the row on what the router speaks says so. A check that runs while a system box stands fails on the clock, since the person can't click at once, and the run after the click succeeds in milliseconds; asking again on ftorrent's own after a box, on the window getting focus back or after some seconds, is a plan of its own, for every box ftorrent can raise.

Good6: moon ​

  • Good6a: Write the public files in good/moon/: the cloud-init file, the coturn configuration, the Go service's source, the service units, the README, and the deploy script.
  • Good6b: Make a key pair for moon alone in .ssh, named by an ssh config file in good/moon/.
  • Good6c: Choose the provider with the live prices in front of us, and create the box in its dashboard: the smallest plan on Debian 13 with both address families, the cloud-init file as its user data, and moon's public key.
  • Good6d: Swap the three parked names in DNS, moon.ftorrent.com with both records and ip4.moon.ftorrent.com and ip6.moon.ftorrent.com with one each, to moon's real addresses, and have the same STUN counting rules on the main server by this evening at the latest, with a one-command read of the coturn container's network totals for a first number.
  • Good6e: Put the STUN counting rules in moon's host firewall from the first evening: the no-target request and reply counters per family, the hashlimit dry-run rule per family, and the hourly cron line.
  • Good6f: Install coturn from Debian's package on moon, running our stun-only configuration under a hardened unit beside the Caddy that already serves moon.ftorrent.com and the two single-family names with its own certificates, and check its binding answer against the main server's byte for byte.
  • Good6g: Write the Go service's reflector, the same contract as ip.ftorrent.com.
  • Good6h: Write the Go service's UDP poke: a few small packets carrying a token to the requester's address at a port the requester names, from moon's own address and port.
  • Good6i: Write the Go service's TCP dial: connect to the requester's address at the named port, optionally send a BitTorrent handshake, and report what happened; rate-limit all three calls per source in the Go service.

The second vantage, a small paid server at another place on the internet, entirely ours. This is the one milestone that is administration rather than measurement, and it is a new kind of work for this project.

We choose the provider on the evening, with the live prices in front of us, and nothing in moon depends on which one it is. What moon asks of a provider is short: static addresses in both families, ideally a routed IPv6 /64 so the namespaces in Good8 have room; a field on the create page for cloud-init user data and one for an SSH key; a web console, the virtual machine's own screen and keyboard, for the night a firewall rule locks out our SSH; a full virtual machine with its own kernel, KVM or Xen, so network namespaces and iptables behave as they do on real hardware; and outbound UDP left unfiltered, since sending packets is moon's job. Whichever it is, the box is Debian 13 with coturn, Caddy, and Go from Debian's own packages, and no container runtime. The main server runs its services in Docker containers because it hosts many things and the containers keep them apart. moon runs only these, the virtual machine is its isolation, and a container runtime would stand between the reader and what moon exists to show: the addresses, the firewall rules, and the namespaces of Good8. So every service on moon is a systemd unit that carries the hardening the main server's compose file gives its containers, in systemd's own directives: an unprivileged user, a read-only system, no capabilities, memory and task limits. coturn runs in stun-only mode on 3478 over both families with the same forty-line configuration the main server mounts into its container, and listens on the host itself, so the STUN counters sit in INPUT and OUTPUT rather than in Docker's chain. The deploy script builds the Go service on the box with Debian's Go, and it runs behind Caddy, which terminates TLS with Let's Encrypt certificates it fetches itself for moon.ftorrent.com and the two single-family names, and binds moon's own addresses, since sending pokes and dials from them is its job. The two boxes differ, then, in their place on the internet and in how their services are run, and moon's guide says why.

We built moon first on Ubuntu 24.04 with Docker, chosen because its updates pocket carries Docker 29 while Debian 13 ships 26, and ran it that way for ten days with Caddy alone on it. On 2026-10-25 we decided to run moon bare, and with Docker gone nothing held Ubuntu: Debian's cloud image is smaller, runs none of the storage, modem, and desktop daemons Ubuntu's does, carries the same coturn and Caddy with Go 1.24 as its default, and never moves a package's major version within a release. The rebuild keeps both addresses, which are the provider's reserved resources rather than the instance's, so every record in DNS stands.

Everything that builds the box lives in good/moon/ and is public: a cloud-init file that makes the box from nothing, the coturn configuration, the Go source, the service units, the firewall's two rules files, a README in the usual shape for a reader with their own provider, and the deploy and firewall scripts. Nothing we run talks to the provider. Create the box once, in the provider's dashboard: paste the cloud-init file in as its user data and add moon's public key. From then on every step is SSH, so the workflow is the same at any provider, and the README tells a reader at another one what to fill in where. We make the key pair for moon alone, in the home directory's .ssh and named by an ssh config file in good/moon/, so the key that opens moon opens nothing else, and the README says in words where a reader puts theirs.

We administer moon directly over ssh: the deploy script, the service restarts, and whatever the evening needs on the box. moon runs nothing else, and if it burns down we rebuild it from cloud-init in minutes. Its units run hardened, unattended upgrades patch it each morning, and a host firewall admits only what its services need.

DNS goes early in the evening, since the certificate needs the name before anything else can be tested: moon's static v4 and v6 go into the zone under moon.ftorrent.com, replacing the documentation addresses parked there, with ip4.moon.ftorrent.com carrying only the A record and ip6.moon.ftorrent.com only the AAAA, the same single-family trick the page already rests on. All three names are parked ahead of time, so the evening's DNS step is an address swap and nothing new is named while the certificate waits. Nothing on the main server ever connects to moon.

The STUN port on both boxes starts being counted the same night. Anything answering on UDP 3478 over IPv4 is swept into reflector lists within days of coming up, announced or not, so the counting starts before anything else does. The instrumentation is small. Four firewall rules with no target, one for requests and one for replies per family, count packets and bytes and change nothing; a fifth per family carries the hashlimit match and no target, counting only packets above a per-source rate, so it climbs only when something is using the server as a reflector, a dry run of the guard; and a cron line appends the counter lines to a file hourly, so a burst shows as a time series rather than vanishing into a total. Where the rules sit differs by box. On the main server, Docker's DNAT carries the traffic to coturn's container past INPUT, so they sit in DOCKER-USER, matched on the container's address, and they have counted there since 2026-10-12. On moon, coturn listens on the host itself, so write them into INPUT and OUTPUT in the firewall's two files from the first evening, and read them by hand. The guard itself, the same hashlimit rule with a drop target on both families, waits on a week of those numbers from both boxes and takes its threshold from them. We write the guide an operator section on all of it once the guard is running.

The Go service offers three things over HTTPS, and one rule governs all of them: moon only ever sends to the address a request came from, never to an address the request names, so it can never be pointed at anyone else. The first is the reflector, the same contract as ip.ftorrent.com, which gives both places a second HTTP vantage and a way to tell "our main server's v6 is down" from "the user has no v6," an embarrassing failure otherwise. The second is a UDP poke: on request, moon sends a few small packets carrying a token to the requester's address at a port the requester names, from moon's own address and a port of moon's choosing, and the requester reports whether they arrived. The third is a TCP dial: moon connects to the requester's address at the named port, optionally sends a BitTorrent handshake, and reports whether the connection opened and what came back. The Go service rate-limits all three per source address. The signaling room for the NAT peers is added in Good8.

Good7: Two vantages ​

  • Good7a: Replace Cloudflare's STUN with moon's as the page's second server, and fetch moon's reflector through ip4.moon.ftorrent.com and ip6.moon.ftorrent.com as a cross-check on the main server's.
  • Good7b: Point the panel's second STUN server at moon.
  • Good7c: Build the panel's filtering check: after the socket speaks to moon's STUN, ask moon to poke the mapped address from an unrelated port, and after it speaks only to the main server's STUN, from moon's address; sort the result into the three filtering kinds, over v4 and over v6.
  • Good7d: Build the inbound proof: ask moon to poke the engine's listen port over UDP and dial it over TCP with a BitTorrent handshake, over v4 and v6, and watch the engine's alerts for what arrives.

With a second public address, the proofs that have been waiting.

On the page, moon's STUN server replaces Cloudflare's as the second entry, and the mapping verdict stands on our own infrastructure. The page also fetches moon's reflector over each family, through the two single-family names, as a cross-check on the main server's, so a failed v6 fetch to the main server with a successful one to moon is reported as our problem rather than the user's.

The panel runs the full behavior-discovery flow without needing RFC 5780 machinery on coturn, because moon can originate packets. Mapping: the same local port asks the main server's STUN and moon's STUN, and the two mapped ports agree or disagree. Filtering, from the same socket: the panel asks moon to poke the mapped address from an unrelated port after the socket has spoken to moon's STUN, and from moon's address after the socket has spoken only to the main server's, and which pokes arrive sorts the filtering into endpoint-independent, address-dependent, or address-and-port-dependent, the three cone subtypes. On Windows the host firewall filters too: without a rule allowing the program in the current network's profile, it should admit a packet only from an address and port the socket has sent to, which would read as address-and-port-dependent whatever the router does, so the panel reads the firewall first, through Good5's read, and says which layer answered. The same flow runs over v6, where mapping is free and only filtering remains, which is the shape worth showing: the v6 result is one question where the v4 result is two.

Then the proofs that matter most. With the engine's session listening and port mapping still off, the panel asks moon to poke the listen port over UDP and to dial it over TCP with a BitTorrent handshake, over v4 and over v6. The v4 attempts are expected to fail on any home NAT, and the panel watches the engine's alerts to see nothing arrive; the v6 attempts prove whether the router's firewall admits unsolicited inbound to a global address, which on many home routers it does not. An incoming_connection_alert that fires because moon dialed is the first time a stranger has reached this client on purpose, and it is the measurement Transmission's port test has offered its users for years, done with our own server and over both families.

Good8: Peers behind our own NAT ​

  • Good8a: Give moon two network namespaces with source-NAT rules we write: one with endpoint-independent mapping and port-restricted filtering, one symmetric.
  • Good8b: Write a pion peer in Go and run one behind each namespace, gathering through the main server's STUN and reporting which candidate pair carried the data channel.
  • Good8c: Add a signaling room to moon's Go service over HTTPS.
  • Good8d: Build the page's punch check: try both peers and report the outcome, the home-router peer only, neither, or both.
  • Good8e: Write a tiny UDP peer in Go and run one behind each namespace.
  • Good8f: Write net_punch in Rust: bind a socket, learn its mapped address from STUN, exchange addresses with the peer through moon, send both ways at once, and report which direction's packets arrived.

It is right to doubt that a WebRTC peer on a public address proves anything about hole punching: the user's own outbound packet opens the return path, so a public peer connects even to a user behind the worst NAT. The answer is to put the peer behind a NAT too, and since moon is ours, to build the NAT ourselves and shape it deliberately.

Give moon two network namespaces, each joined to the host by a veth pair, two virtual Ethernet interfaces wired to each other, and translated by a masquerade rule we write in the firewall's file, one plain and one with random ports, since the two kinds of NAT differ by that one flag. One namespace translates like a home router, keeping the same public port regardless of destination and admitting replies only from the address and port already spoken to: endpoint-independent mapping with port-restricted filtering, the common case. The other picks a fresh port per destination: the symmetric case. Behind each sits a pion peer in Go, which gathers its reflexive candidate through stun.ftorrent.com on the main server, since a packet from a namespace to an address on moon itself is delivered locally and never crosses the masquerade, so moon's own STUN would see the private address. It exchanges descriptions with the user through a signaling room in moon's Go service over HTTPS and opens a data channel, reporting which candidate pair carried it.

The page tries both peers. Connecting to the home-router peer and not the symmetric one is what endpoint-independent mapping with ordinary filtering looks like, and it is the hole punch demonstrated rather than inferred from STUN. Connecting to neither is the symmetric user. Connecting to both is a user whose NAT admits unsolicited inbound once any mapping exists, which is a thing the symmetric peer can teach that the first cannot. Each outcome also proves the user's browser can run WebRTC end to end, DTLS and SCTP included, which some networks and some privacy extensions prevent.

The panel does the same punch by hand, with no WebRTC: a tiny UDP peer in Go behind each namespace, which learns its mapped address the same way, and a net_punch command in Rust that binds a socket, learns its mapped address from STUN, hands it to moon over HTTPS, receives the peer's mapped address back, and then both sides send to each other at once and report which direction's packets arrived. That is the technique the How Can Two Peers Connect? essay describes, in about a hundred lines on each side, and watching it succeed through one NAT and fail through the other is the evening's whole purpose.

Good9: Changing things, with the user's say-so ​

  • Good9a: Turn libtorrent's two mappers on through the port_mapping setting, and show the portmap alerts' protocol and external port.
  • Good9b: Send a hand-written NAT-PMP mapping request with a one-minute lifetime and delete it afterward.
  • Good9c: Send a PCP MAP request with a one-minute lifetime and delete it afterward, recording whether the router speaks PCP at all.
  • Good9d: Attempt a v6 pinhole over PCP and over UPnP's AddPinhole through igd-next, expecting refusal and recording it.
  • Good9e: Add the Windows firewall rule through an elevated netsh run the user approves, for an installed copy only.
  • Good9f: On macOS report the firewall's standing and what the user can do by hand.
  • Good9g: Re-ask the facts the OS can change behind our back when the window comes forward.

Now the panel changes the machine's standing, one thing at a time, and re-runs the proofs after each so every change shows its before and after.

The router first. libtorrent's two mappers turn on through the port_mapping setting the desktop planning doc describes, and the portmap alerts report which protocol won and what external port it gave; moon then dials and pokes the listen port again, and a connection that failed an evening ago succeeds, or does not, and the panel says which. Beside libtorrent's mappers, the panel's own hand-written NAT-PMP mapping request and a PCP MAP request, with lifetimes of a minute and an explicit delete afterward, show the two protocols on the wire and whether the router speaks the newer one; PCP is thought to live mostly in carrier equipment and enterprise gear, and the evening finds out what the router at hand says. libtorrent's NAT-PMP mapper turns out to send PCP first and fall back to NAT-PMP, and its port-mapping log shows which one answered, so the evening starts by deciding whether these two requests still earn their place beside it. The same evening tries the v6 side, where the question is a firewall pinhole rather than a mapping: a PCP MAP request over v6 to the router, and UPnP's IPv6 firewall control action, AddPinhole, from the IGDv2 service, which igd-next carries. NAT-PMP has no v6 at all. The expected result on a home router is refusal or silence, and the refusal is the finding: an earlier draft of the essay called the v6 firewall the simpler problem, and it is for punching and is not for listening, which the panel proves here before moon dials the listen port over v6 once more.

The firewall second. On Windows the panel adds the rule the desktop planning doc already specifies, through an elevated netsh run that the user approves, for an installed copy only, first removing any block rules the security alert left for the same program, since a block overrides an allow, and re-runs the inbound proofs. On macOS the panel changes nothing, since there is no supported way to add an application to that firewall, and instead reports whether the firewall is on and whether it admits the engine, and when it doesn't, where the user makes the exception themselves; the proof re-runs after they do it. A Windows copy with no rule, a portable one or one whose user declined, gets the same report. The facts the OS can change behind our back are re-asked when the window comes forward, as planned.

Nothing here is required, and the panel keeps saying so: a user who declines runs outbound-only and the client keeps working, less well, with the panel showing exactly how much less.

Good10: In concert ​

  • Good10a: Label every result proven, inferred, or unknown.
  • Good10b: Cross-check the HTTP address, the STUN address, the router's external address, libtorrent's peer-voted address, and the interface list against each other, and name what a disagreement exposes.
  • Good10c: Apply the two axes, self-knowledge and reachability, as separate lines, and state one carrier-grade NAT likelihood with its reasons beside it.
  • Good10d: Produce one self-describing document per run from both places, each entry carrying its value, its method, and its vantage, the page's a subset of the panel's.
  • Good10e: Write the three-tier rating as one draft function over that document, not a screen.

The last evening builds nothing new on the wire and instead reasons across everything the others measured.

Label every result with what kind of claim it is: proven by a packet that arrived, inferred from a comparison, or unknown because the platform could not say. The cross-checks go in: the HTTP address, the STUN address, the router's reported external address, libtorrent's peer-voted external address, and the interface list, which between them expose a relay, a second translator, a translator the carrier runs, or nothing unusual. The two axes, self-knowledge and reachability, are applied explicitly so that "you have IPv6" and "a peer can reach you" stay separate lines, and NAT64, shared address space, mapping, and filtering feed one stated likelihood of carrier-grade NAT with its reasons beside it.

Both places produce the same thing at the end of a run: one self-describing document per run, each entry carrying its value, how it was measured, and from which vantage, with the page's document a subset of the panel's. A three-tier rating, kept as a candidate, is written as one function over that document and left at that, a draft rule set rather than a screen, since deciding whether and how to show a rating is the product phase's question. The spike ends with the dossier: everything we can honestly tell a person about their connection, from both places, with the basis for every line stated. The articles, the design, and the presentation start from there.

After Good10: what it means, and what to do about it ​

Good10 ends this plan, and it does not end good. The ten milestones are the engineering spike: they build every measurement, prove each one against real networks, and report it plainly, until a run from the page or the panel produces a record of a connection that we can stand behind line by line. That record is the foundation, and on its own it is still a list of facts. Two stages follow it, in order. We will productize the page and the panel, so that each tells a person what their facts mean, and then give the panel the features that change a connection's standing, the firewall rule and the port mapping, made for the person with their say-so. The desktop client's shell is built, with installing, updating, and settling into each system done, so both stages sit on the near horizon rather than somewhere past it.

What the facts mean ​

A person opens good to learn what their facts mean for them, and the first stage answers that: explaining what was measured, picturing it, sorting it into kinds a person recognizes, and documenting each kind, so that someone who runs good leaves understanding their own connection.

It speaks in three parts and never blurs them. The facts are certain, each one proven by a packet that arrived or read from the machine or the router: the router reports one external address while the reflector sees another, and two STUN servers asked from the same local port report two different mapped ports. The interpretation is named as one, in a few words, and stated as plainly as the evidence allows: a second translator sits above the router, which very likely means the provider runs carrier-grade NAT, and the path out gives each destination a fresh port, the symmetric kind. The outcome is what the person came for: over IPv4, no peer can open a connection to this machine and a hole punch will fail, so ftorrent reaches its peers by connecting out and downloads still work, while being reached directly rests on IPv6, which the same run answers on a line of its own. A fact never borrows the confidence of a reading, and a reading never hides the facts it stands on.

That stage is a body of work as large as this one. It needs a catalog of patterns, each a signature in the facts, the reading it most likely has, and what it changes for a person; kinds of connection a person can find themselves among, like a phone on cellular data, a home router, a VPN, or an office network; pictures that show a translator or a firewall rather than describing one; articles on docs.ftorrent.com that teach each pattern once, so the page and the panel link to the lesson instead of repeating it; and the choosing and ordering of what a person sees first, which is a product decision rather than a measurement. None of it belongs to this plan's milestones, and none of it can be done well until the facts beneath it exist and have held up on real networks.

What a person can change ​

Understanding a connection is half of what good can offer, and the other half is changing it. Most of what stands between a machine and its peers can be adjusted, and much of it by software, once the person agrees.

The machine's own firewall comes first, and the first part of that feature is noticing it: ftorrent reads whether the system's firewall is on and whether it admits the engine, and when it doesn't, tells the person so and where to make the exception themselves. The macOS firewall ships turned off, so most Macs need nothing. With it on, it asks the first time the engine listens, since its allowance for signed software doesn't cover ftorrent, and it gives an app no supported way to add itself, so the panel points to the prompt or to System Settings and checks the result afterward. The Windows firewall ships turned on and shows its alert the first time each program listens, ftorrent itself at the Connection tab's first Check and the engine when its session first listens. An installed copy can carry a rule naming the engine, added through an elevated run the person approves, and a copy without one gets the same report a Mac does.

The router comes second, and a home router may speak any of three protocols for this. UPnP is the oldest and the most widely supported. NAT-PMP is Apple's lighter protocol. PCP, the Port Control Protocol, succeeded NAT-PMP and adds IPv6, and in principle it reaches a carrier's translator as well as the home router. Over IPv4, each asks the router to forward a port to this machine for a stated time. Over IPv6, where nothing translates, the same kind of request asks the router's firewall to open a pinhole instead, through PCP or through UPnP's IPv6 firewall control, a request Good9 expects most home routers to refuse.

Past what software can reach sit the steps only the person can take, which the panel explains and leaves to them: a port forward typed into the router's own settings page, IPv6 switched on in the router, or a public IPv4 address asked of the provider, which some providers offer in place of a shared one.

Good9 makes each software change once, for a minute, and undoes it, to prove the mechanism and measure its before and after. The features are the finished form. A mapping stays renewed while ftorrent runs and is released when it quits; a firewall rule lives with the installed copy and leaves with it; and the panel makes each change only after the person says yes, shows what it did, and undoes it on request. libtorrent already carries a UPnP mapper and a NAT-PMP mapper, so part of this stage is turning on what the engine has and reporting what each mapper did, in the same three parts as everything else good says.

The spike ends with everything we can honestly say about a connection. The stages after it say what that means to the person whose connection it is and then help them change it, and that is what good is for.

Some rights reserved: Creative Commons Attribution 4.0 International

ftorrent licenses the text of this page under Creative Commons Attribution 4.0 International.