Releases: moshcoder/moshcode
Release list
v0.16.2 — a resolver that can defend itself
Two changes, both prerequisites for a resolver that strangers can reach.
The bridge listens on IPv6, not only forwards to it
It picked the socket family per upstream when forwarding and always bound udp4 when listening — a strange shape for a namespace whose targets must be IPv6: the resolver for a v6-only network could not be reached over v6.
The family now follows the address it is asked to bind, so :: serves both from one socket. The default stays loopback IPv4, so upgrading does not widen what a machine answers on — serving anything else is a deliberate act.
The resolver can defend itself
An open forwarding resolver is a DDoS amplifier before it is anything else, and the attack needs no botnet: one host spoofs a victim's source address, sends a small query, and the resolver mails the large answer to the victim.
That shape rules out most defences. The source address is a lie, so blocking "the client" punishes the victim; there is no session to fingerprint and no user agent to read. What is left is bounding the amplification one query can buy, and what one source can extract before we stop answering it:
- ANY is refused (RFC 8482) — a 30-byte question for every record a name holds, which is the lever the attack pulls
- A token bucket per source decides what counts as an offence
- Offences earn a ban that doubles each time, capped, and decaying after a clean spell. A flat limit is a toll an attacker pays and returns from; backoff makes persistence expensive while a client that misbehaves once loses a minute
- Oversized forwarded answers are truncated with TC, sending a real client to TCP — where the handshake makes a spoofed source useless. Legitimate case retries, abusive case dead-ends
Refusals are REFUSED rather than silence: a dropped packet costs a real client a full resolver timeout before it tries elsewhere, and costs an attacker nothing.
IPv6 clients are grouped by /64, and this is the part that decides whether any of it works. A single v6 address is free to change: any host worth banning holds a /64 at minimum, so a per-address ban is defeated by incrementing — silently, because the bans look like they are being applied. IPv4 stays per-address, because a /24 routinely spans unrelated customers behind carrier NAT.
Both tables are bounded and evict least-recently-seen. The key space is attacker-controlled, so an unbounded map is the vulnerability rather than the mitigation.
Nothing changes for an existing install
The listener default is unchanged and the guards require explicit configuration. This releases the capability for a deployment to use; it is not a behaviour change you would feel.
If Moshpit names still fail to resolve on your machine, v0.16.1 is the release that fixes that.
v0.16.1 — routing stopped being a list, for real this time
v0.16.0 shipped the catch-all routing machinery and wired none of it in, so upgrading to it changed nothing. This is the version that actually changes routing.
What v0.16.0 got wrong
dns enable still called resolvedConf(tlds) — every claimed ending on one Domains= line. systemd-resolved accepted about 1,090 of them and dropped the rest with no error a caller could see, so moshcode dns resolve <name> answered while curl <name> could not resolve. The v0.16.0 notes said routing had stopped being a list. It had not.
What changes now
Routing is written conditionally, never assumed:
| upstreams found | routing written | worst case |
|---|---|---|
| yes | Domains=~. — one line, nothing to truncate |
Moshpit names fail |
| no | the per-ending list, exactly as before | Moshpit names fail |
Upstreams are read from /etc/resolv.conf before routing is switched, because afterwards it may point at the bridge and the real servers are no longer discoverable from it. Loopback entries are dropped so the bridge cannot forward to itself. Neither branch can take a machine's DNS with it — pointing every lookup at a bridge that cannot forward is a far worse failure than a Moshpit name that does not resolve.
With upstreams configured the bridge answers claimed endings and relays everything else byte for byte, failing open in every direction: an unknown ending set means "not ours", and silent upstreams are SERVFAIL rather than NXDOMAIN, so a lookup is retried elsewhere instead of cached as nonexistent.
Also: the resolver stays up when its socket errors after bind.
Why the list had to go
4,593 endings in the morning, 4,882 by the afternoon, against a resolver cap near 1,090. Every claim made it worse, which is why the fix is not "raise the limit".
Upgrading
moshcode update # as root
moshcode dns enable # as root
If a bridge from an older build is already listening on port 5354, dns enable will report "bridge already running" and leave it in place — an older bridge does not forward, so stop it first.
v0.16.0 — routing stopped being a list
install.sh resolves releases/latest, so the twelve commits since v0.15.0 have been unreachable to moshcode update — including the reason curl <name> still failed after enabling DNS.
Routing stopped being a list
dns install wrote every claimed ending onto one Domains= line. systemd-resolved caps how many search domains it accepts:
| written | 4,586 |
| accepted | 1,090 |
| rejected | 3,496, one journal line each |
systemd-resolved: Failed to add search domain '~zombies', ignoring: Argument list too long
Taken alphabetically, so endings past the cut were configured on disk and absent from the resolver — moshcode dns resolve answered while curl could not, with nothing in between to say why. Every new ending made the cut deeper.
Domains=~. is one entry that never grows. The bridge now forwards anything that isn't a claimed ending upstream, byte for byte, and fails open in every direction: an unknown ending set means "not ours", silent upstreams are SERVFAIL rather than NXDOMAIN, and loopback nameservers are dropped so it can't forward to itself.
moshcode dns status now reports what the resolver accepted, not what was written — the check that had been comparing two numbers we control and agreeing with itself.
Also in this release
- IPv6 end to end — the bridge answers
AAAA, and anAquery for a v6-only name is NODATA rather than NXDOMAIN, which had been denying the name for the whole page load. The/n/gateway brackets v6 hosts, which it previously built as invalid URLs. moshcode template— scaffolds a Moshpit-hosted service;bun-caddy-sqliteandcaddy-staticbundled. Nothing in a template executes on install.- Letters and digits only — a dash was the cheapest way to mint a look-alike of an ending someone else holds, in a namespace one level deep with nothing to appeal into.
- An ending has its own page at
/n/<ending>, showing its pointers and relatives instead of a dead end.
Upgrade
moshcode update # as root
moshcode dns enable # as root
v0.15.0 — curl the name
install.sh resolves releases/latest, so the ten commits merged since v0.14.0 were unreachable to anyone installing — including the reason curl <name> didn't work.
The headline is parking
A parked name always resolved somewhere, but the address it pointed at was a host that routes by Host header and answers "Application not found" for a name it has never heard of. curl scrambled.eggs resolved and then died one layer up.
It couldn't be fixed there — the platform rejects a Moshpit ending as a custom domain, and no public CA will certify a TLD outside the DNS root. The bridge is already running locally for the name to resolve at all, so it now serves the answer too:
$ moshcode dns start
parked names → http://127.0.0.1:80 → https://pit.moshcode.sh/n/<name>
$ curl -i scrambled.eggs
HTTP/1.1 302 Found
location: https://pit.moshcode.sh/n/scrambled.eggs
Port 80 needs privileges; a failed bind falls back to the old behaviour with a clear message rather than refusing to start. --parking-port N moves it, --no-parking-http turns it off.
Underneath it, a quieter one
fetchTlds read the first page of the ending list and stopped. The registry answers 200 rows and reports the real total, but 200 rows look exactly like a complete list of 200. .eggs sat past that line, so dns install wrote a config that didn't route it and the name failed to resolve — a DNS-shaped symptom three layers from the cause.
It was hiding 94% of the namespace: 3707 endings, 200 visible.
Also in this release
dns resolvereports a parked name's page in the Pit instead of an IP that answers for nobody, with--opento go there- the pit's
/n/pages are crawlable — robots.txt, a generated sitemap, and canonical tags naming the pit host rather than the app host it shares a service with - the claim box takes a whole name, claiming the ending first when it's free and minting the name under it
- the endings list pages instead of stopping at 200, the paste field reads names as well as endings, related endings keep the name you're reading, and integrations ship JSON support matrices
Upgrading
curl -fsSL https://moshcode.sh/install.sh | sh
Then, as root (the resolver edits system DNS, and port 80 is privileged):
moshcode dns install --write && systemctl restart systemd-resolved
moshcode dns start
v0.14.0 — the pit stops locking up
install.sh resolves releases/latest, so the seventeen commits merged since v0.13.3 were unreachable — including a fix for a page that locks browsers up.
The pit
/pit rendered every ending an account held, and a form per name under each, with no bound on either end. At 50 endings × 100 names that was 3.1 MiB of HTML and 36,082 DOM elements — and it jammed browsers with no script on the page at all (#167).
It now draws a window and states what it isn't drawing: 173 KiB, 1,926 elements. With a filter over the top — eggs as a substring, def* as a glob, debounced against the API, and readable server-side from ?q= so it still works with the script blocked (#168).
The namespace stopped being the one part of the product a script couldn't touch: /api/moshpit/* now accepts the same API key /api/me and /api/sessions already did (#169).
/pit/dns documents the TronBrowser route, for machines whose DNS isn't theirs to change (#165).
Pricing stopped lying: $2 a name is a default, not a ceiling. The server always allowed more — the claim form shipped max="2" and made it look like policy (#170).
Also in
- db — foreign keys enforced, and the licence
package.jsonclaims actually ships (#154) - cli — help aliases exit 0 (#157) · invalid integration commands fail (#160) ·
--is honoured (#159) · a BOM before a shebang no longer breaks (#158) - skills — engines with no skills primitive are reported rather than dropped (#166) ·
--namerequires a value (#156) - mcp — an unsupported flag is rejected instead of registered as the server name (#164)
- pit — namespace rules vendored again, with a drift test holding them to the published package (#161, #162, #163)
- prd — a backslash in a title no longer eats the index row's columns (#155)
Install
curl -fsSL https://raw.githubusercontent.com/moshcoder/moshcode/main/install.sh | shmoshcode v0.13.3
What's Changed
- moshpit: publish the keys a name may present by @ralyodio in #137
- pit: claim a pasted list of endings by @ralyodio in #138
- dns: enable Moshpit names on the machine, on any OS by @ralyodio in #141
- pit: let a pasted line carry its own price and target by @ralyodio in #142
- Add Bash, Zsh, and Fish completions by @phucnguyen1707 in #140
- fix(moshscript): keep
codein the result under --dry-run by @clawedassistant26 in #139 - pit: raise the paste ceiling to 1000 and stop on the clock, not a count by @ralyodio in #143
- pit: $2 a name, $5 an ending — no trailing cents by @ralyodio in #144
- pit: claim a pasted list in batches, not one round trip at a time by @ralyodio in #146
- pit: serve a Moshpit name at /n/ by @ralyodio in #145
- pit: let an ending be all digits by @ralyodio in #147
- pit: buy an unclaimed name from the page someone landed on by @ralyodio in #148
- pit: a parked name is a page, not a 404 by @ralyodio in #149
- moshcode: uninstall an engine or tool by @ralyodio in #150
- fix(completion): complete the uninstall command and its targets by @clawedassistant26 in #151
- pit: an ending can be bought and renewed by @ralyodio in #152
- chore(release): v0.13.3 by @ralyodio in #153
Full Changelog: v0.13.2...v0.13.3
v0.13.2 — turso where you left it
What's Changed
- fix(tools): find turso in ~/.turso instead of reporting it missing by @ralyodio in #135
- fix(tui): stop styling the agent-view notice as an error by @ralyodio in #134
- fix(runtime): deliver fire-and-forget notify() when the script throws by @clawedassistant26 in #133
- chore(release): v0.13.2 by @ralyodio in #136
Full Changelog: v0.13.1...v0.13.2
v0.13.1 — upgrades that actually upgrade
moshcode upgrade no longer leaves a target stranded when its own updater refuses.
opencode-family updaters decide how to update by recognising where the binary was installed. When they don't recognise the location they report Using method: unknown and stop — the same on every run, so the version never moves. The same opencode upgrade reports Using method: curl and succeeds where the install is one it knows, which is why this failed on one machine and worked on another.
- when a native updater fails, the target's installer runs instead — installers are idempotent and fetch the latest, which is already what an uninstalled target uses
- the fallback only exists where the updater is a different command, so it can never repeat the one that just failed;
claude,codexandgeminiarenpm i -gand aren't retried - it says when it falls back rather than retrying silently
- privacycode drops its native updater outright: it's opencode's, pointed at
~/.privacycode/bin, so it could never upgrade an install made by its own installer
v0.13.0 — moshcode dns
moshcode dns — resolve Moshpit names on the machine, not just in the browser.
The registry speaks HTTP, not DNS: pit.moshcode.sh answers /api/moshpit/resolve and nothing listens on port 53, so curl https://california.oranges/ failed on any server. A browser extension can't fix that — redirecting tabs isn't resolution.
moshcode dns tlds # the TLDs claimed in the Pit
moshcode dns resolve <name> # what a name resolves to, and why
moshcode dns start # run the resolver
moshcode dns install [--write] # systemd-resolved / dnsmasq config
Claimed-but-unpointed names answer with the parking host, so they explain themselves instead of returning NXDOMAIN. A registry that is unreachable still returns NXDOMAIN — an outage must not silently point every Moshpit name on the machine at a parking page.
17 new tests including a real UDP round-trip; full suite 331 pass / 0 fail.
v0.12.3 — paste a block
Paste a block of commands into the session page. The send box was a one-line input, so anything multi-step had to be sent a line at a time — and pasting a block dumped it onto one line, which then ran as a single nonsense command.
- it's a textarea: paste grows it, enter runs, shift+enter makes a new line
- the split is server-side, one queued command per line, so it works with the CLI you already have — the pit takes exactly one line per prompt turn
- lines are stamped a millisecond apart, so a paste lands in the order you pasted it
- blank lines and the trailing newline of a copied block are dropped rather than fired as bare enters; 50 lines per paste
the pit — names can be bought under someone else's TLD, paid via CoinPay (#127); numeric hostname labels and numeric labels when registering are both accepted now (#119, #120)