Repository navigation
v0.6.0
Pre-releasePre-release: this is a 0.x version and may change without notice.
Added
- Docs and tooling: website: the plain pages (index, in action, discuss) are laid out like the undecuple page: under the banner a box of key and value rows with dotted leaders (project, version, features, repository) beside a box of links, double-framed tables with alternate-row shading, and section headings with a block of four greys. The sticky title bar and the framed section legends are gone, and the menu entries are now lower case (
in action,discuss). The terminal view keeps its layout and hides the new block; its buttons ([F1 help],plain view, the command chips) and the menu tabs now have a visible outline, as have the four entries of the legend on the "in action" page. The "in action" and "discuss" pages offer a "Main page" link at the top and the bottom and, when the reader came from another page of this site, a "Previous page" button (site/nav.js, same origin, nothing stored or sent). The font list is now the undecuple page's (local fonts only, ligatures off), so the two sites look alike on one machine; line spacing stays at 1.5. The change is insite/style.css,tools/site.py, the newsite/nav.jsand the generated pages; the Content-Security-Policy of every page is unchanged (nav.jsis a same-origin script). - Docs and tooling: website: the terminal view has a
discusscommand and a "Discuss" menu entry.discusssays what the discussion page is and links it (discuss.html), the Discussions on GitHub and the issues; like the page, the command loads nothing from another site. - Docs and tooling: website: in the terminal view the name in the header of an answer now ends in
.md(about.md,features/rooms-basic.md,settings/addr.md), and the newest answer fills the pane, with the line-number gutter's rule running to the bottom, like a file open in a viewer. Styles and file names only; no new command and no change to what an answer says. - Docs and tooling: website: a discussion page (
site/discuss.html, "Discuss" in the menu): one GitHub Discussions thread (repositoryundecuple/me, category Announcements, termre-psn) shown with giscus. Nothing is requested from giscus.app or GitHub until the reader presses "Load the discussion";site/discuss.jsthen adds the giscus script. This is the only page whose Content-Security-Policy allows another site (https://giscus.appfor scripts, styles and frames); the other pages keep the strict policy, and a test checks both. The settings of the thread are intools/site.py; no setting of the repository was changed by this. - Docs and tooling: website: the terminal view has an
actioncommand and an "In action" menu entry.actionlists the four simulated scenarios;action direct,action punch,action relayedoraction blockedprints that scenario's numbered steps with the message names, and every answer links to the animated page. The text comes from the same data assite/action.html, so nothing is added to the page's data. - Docs and tooling: website: a "see it in action" page (
site/action.html, linked from the page's menu as "In action"): four simulated scenarios (direct, hole punch, relayed, relay off), each a diagram and numbered steps that name the real messages (HELLO,PROBE,PUNCH,ROUTE,STREAM_CONNECTand so on). The script istools/site_action.py: documentation addresses only, no live traffic, no script on the page, and a test checks every message name againstdocs/api.md. With script (site/action.js, same origin, no library) one scenario shows at a time and a packet moves along the diagram: Play/Pause, Back, Step, Restart, a speed choice, a clickable step list and the keys Space, Right, Left and R; with a reduced-motion setting nothing moves by itself and Step and Back show each step at once. - Client:
psnr_set_connect_policy(fn, user)(client/psnr.h): the host registers one callback that the C client asks before it opens a TCP connection, so a runtime that restricts where a game may connect can refuse it. It is asked for the server connection (once per resolved address, with the host as typed) and for each relay leg; it returns nonzero to allow and 0 to refuse. If every address is refused,psnr_connectreturns NULL andpsnr_connect_error()is the newPSNR_E_POLICY(1000, set by the client, not sent by a server); a refused relay leg returns -1. No callback, the default, changes nothing.psnr_discoverand the title's own P2P socket are not covered. - Server: a capability report (#77, item 3):
GET_CAPS(0x04, no body, after HELLO) is answered withCAPS(0x94):bits u32and the server version as a blob. The bits say which features the server offers this player: rooms, scores, room codes, peer keys, the UDP probe, the relay and LAN discovery; the probe and relay bits are per title, so a title in-no-peer-titlessees them clear. A server before this answersGET_CAPSwith error 1. The C client hasPSNR_GET_CAPS,PSNR_CAPS, thePSNR_CAP_*bits andpsnr_caps_parse. Seedocs/api.md, "Capabilities". - Deploy:
docker/docker-compose.ymlanddocker/docker-compose.host.ymlnow run the container as user 10001 with all capabilities dropped,no-new-privilegesand a read-only root filesystem with a 16 MB tmpfs on/tmp, the linesdocker-compose.test.ymlalready had. A log file (RE_PSN_LOG_FILE) inside the container needs a volume, and a listen port below 1024 cannot be bound. Memory, CPU and process limits stay in the test file only. - Deploy:
deploy/re-psn.servicesetsLimitNOFILE=16384, so a server at the default-max-conns(4096, a relayed stream counting two sockets) stays under the file-descriptor limit.scripts/check-deploy.shchecks the line. An install that copied the unit earlier keeps its old limit until the file is copied again. - Network:
-relay-idle(RE_PSN_RELAY_IDLE, default 600 seconds; 0 for never) closes a relay TCP stream that has carried no bytes in either direction for that long, and a write to a stream leg that the peer does not read is dropped after 30 seconds. Before, a relay stream cleared its deadlines and stayed open as long as both legs did, so idle or stalled streams held connections (now counted by-max-conns) indefinitely. A stream that keeps moving bytes is not limited in time or size. - Docs and tooling:
docs/integrating.mdhas a "Mapping the NP calls" section: suggested NP call families to re-psn message tables, a list of what the protocol does not cover (world lists included), and a recommended NP error category for each protocol error code. It is suggested, not tested in a title, and cites sections instead of line numbers. - Server:
-max-conns(RE_PSN_MAX_CONNS, default 4096; 0 for no limit) caps the open TCP connections in total, players and relay legs together. Until now only-max-conns-per-addr(32) bounded them, so many addresses could open connections until the process ran out of file descriptors. A connection over the cap is closed without an answer, counted in/api/stats(limits.max_conns,tcp_refusals.conns) and in the admin state, and logged at warn level at most once a second. A server that really holds more than 4096 players at once needs a higher value (and a file-descriptor limit above it). - Server:
-no-peer-titles(RE_PSN_NO_PEER_TITLES, off by default): a comma-separated list of communication IDs (1 to 12 printable ASCII bytes each; anything else stops startup) of titles that use no peer traffic. For a player of a listed title the server answers no UDP probe and records no UDP endpoint, drops relay datagrams from or to the player (counted underrelay_disabled), answers a relay stream with the "no relay" error (9), pushes no relay route even with-relay-force, and clears the relay flag (bit 0) in HELLO_ACK. Rooms, scores and every other title on the same server are unaffected. One list turns off the probe and the relay together; LAN discovery is not covered. Seedocs/running.mdanddocs/side-service.md. - Server: Experimental, read-only admin API, off by default:
-admin ADDR(RE_PSN_ADMIN) opens a second listener with one route,GET /admin/v1/state, for a read token kept in a file (-admin-read-token-file; at least 32 characters, mode 0600, read again on SIGHUP). An address that is not loopback needs-admin-tls-certand-admin-tls-key(TLS 1.2 or newer). The state is aggregate only (version, uptime, relay, limits, refusal counts, the ban count, which is 0 for now): no address, player or token. A wrong or missing token gets one identical 401 after a growing, capped delay and one log line that carries only the socket's remote address, at most 8 of the 32 connections go to one source, anOriginheader, or a name inHostover plain HTTP, is refused. There are no actions yet, and the status page and/api/statsare unchanged. See docs/admin-api.md. - Server: Medius compatibility switch, off by default:
-medius(RE_PSN_MEDIUS),-medius-port(default 10071) and-medius-addr(default127.0.0.1; a loopback, private or link-local address, or0.0.0.0/::in a container). No Medius protocol is implemented: with the switch on, the server opens that port and closes every connection to it without reading or writing, and says so in a start-up warning. A port or address other than the defaults is refused without the switch. Commented examples in both Docker compose files,deploy/re-psn.env.exampleanddocs/running.md. - Client: constants for
PEER_AUTH,SET_ROOM_ACCESS,JOIN_BY_CODE,GET_ROOM_ACCESS,ROOM_ACCESSandPEER_KEY, and
bounds-checked decoders for ROOM_JOINED, member entries, ROOM_LIST and RANKING (psnr_room_joined_parseand friends). - Client:
psnr_connect_timeout(...)(client/psnr.h):psnr_connectwith a time limit. The TCP connect is non-blocking and the wait for HELLO_ACK is bounded, both by onetimeout_msbudget; a stalled server gives NULL instead of blocking the caller. DNS is still blocking and not covered.psnr_connectis unchanged. - Client:
psnr_dead_reason(c)andpsnr_last_os_error(c): why the C client's connection was lost (peer closed, I/O error, out of memory, send timeout) and the OS error at that point. The first failure is kept. - Client:
PSNR_SEND_TIMEOUT_MS(5000, overridable before includingpsnr.h): the most time one write may wait for the socket to accept data, in total. - Docs and tooling: CI compiles the C client (
client/psnr.c,client/examples/minimal.c) with MSVC on Windows and does not run it there; the build instructions indocs/integrating.mdgive the MSVC command.
Fixed
- Server: wrong or malformed invite codes are now also counted by address, so opening a new connection no longer starts the count again: 20 misses in 60 seconds from one address (an IPv6 /64 counts as one) close the connection, besides the existing 5 per connection. The higher limit allows for players sharing a router.
- Server: a player's connection no longer reads its title without the server lock while a new connection from the same address takes over its name. The race detector flagged the unlocked read; the takeover itself is unchanged.
- Server: a connection that sends a frame header declaring a long payload and then stops no longer costs the server the declared size (up to 64 KB). The payload buffer starts at 2 KB and grows with the bytes that arrive, so memory held by idle half-sent frames is bounded by what was sent.
- Server: a relay stream whose accepting side arrived just as the connecting side's 10 second wait ended no longer leaves the accepting connection open. The accepting leg is now used if it had already claimed the stream, and the connection is closed otherwise; before, the accepting player could be told the stream was ready while the connecting player was told it was not.
- Server: the in-memory score store now keeps boards for at most 1000 titles, next to its existing 1000 boards per title and 10000 players per board. A RECORD_SCORE that would start a board for a 1001st title gets ERROR 6 and is not kept; titles that already have boards, and players already on a board, are unaffected. Before, the number of titles was not limited. The limit is fixed, not a flag. See
docs/api.md, Leaderboards. - Server:
-max-conns-per-addrnow counts an IPv6 address as its /64 (an IPv4-mapped address as the IPv4 address), as the UDP limit and the admin failure delay already did. Before, it counted each exact IPv6 address, so connections from different addresses of one network each had a full allowance. The address a player is known by (rooms, peers, logs) is unchanged. Players behind one IPv6 /64, such as one LAN, now share the limit, as players behind one IPv4 address always did; raise-max-conns-per-addrif that is too tight. - Server: the
-httpstatus page now closes a connection that does not finish its request header within 5 seconds, its request within 10 seconds, does not read its answer within 10 seconds, or sits idle for 30 seconds. Before, the status server had no such limits. - Server:
SET_ROOM_DATAno longer lets the owner make the room's own join reply too big for one packet: a write to the external or internal blob that would is refused with error 6 and changes nothing, so a member who asks for the room again always gets an answer. Before, such a write was accepted and later joins and re-joins were refused. - Server: A search no longer disconnects the player when the rooms' data is large: the reply lists only as many rooms as fit in one packet, and the player pages on from there.
- Server: A player whose connection stops reading no longer freezes the other players in its room: the server drops it after 10 seconds of a stuck write (
writeTimeout), so its room mates' requests get their replies and the stuck player is removed from the room. - Server: A second HELLO on one connection gets error 1 and changes nothing, and a name is held by the player's full IPv6 address: before, all IPv6 players shared one address, so any of them could take another's name.
- Server: A room whose members' data, with the room's blobs, would make its join reply too big for one packet no longer breaks the joiner: JOIN_ROOM and JOIN_BY_CODE answer error 6 and the joiner is not added, instead of the connection being closed after the other members were told of it.
- Server: a player with no IPv4 address (an IPv6 connection) is no longer treated as being on the same network as every other such player. Before, two IPv6 players both had the address 0.0.0.0, so each was handed the other's TCP-derived address instead of its probed UDP endpoint, and a relay stream between them was never chosen. Now they are never on one network, so the probed endpoint is announced and, with
-relay, the relay can be chosen for them. This only shows when such a player has a recorded UDP probe, which needs an IPv4 UDP source and-udp-any-source, since a probe from an address other than the TCP one is otherwise ignored. - Server: A score server no longer slows down or grows without bound under many boards: the memory store keeps at most 1000 boards per title and 10000 players per board, and a score that would pass either gets ERROR 6 (an existing player can still improve their score). Ranks are read from a sorted list instead of sorting the board on every request, and a GET_RANKING_BY_ID with more than 100 ids is refused before its ids are read.
- Client: The C client has
psnr_relay_unwrap_from, which accepts a relayed datagram only from the server's UDP address. A title that
reads relayed datagrams withpsnr_relay_unwrapcan be sent a forged sender by any room member;psnr_relay_unwrapis kept
unchanged for compatibility and documented as not checking the source. - Docs and tooling: a comment in
server/rooms.gothat pointed at the missingdocs/room-codes.mdnow names the section ofdocs/api.mdthat holds the content; SECURITY.md states that players behind one address can take over each other's names, and the docs say that-max-req-ratedoes not bound the in-memory score store (which has its own fixed limits per title and board), and that peer keys give no replay protection on their own. - Client: the receive path checks its allocations (a failure now ends the connection instead of dereferencing NULL), a reply
frame too short to hold its request id is dropped instead of being delivered as a push, andpsnr_connectrefuses a
comm_idover 12 or anonline_idover 16 bytes instead of cutting it. - Client: in the desktop app,
r(refresh) on Rooms, Leaderboards and Server does nothing while a fetch is running and for one second after its answer. Every fetch opens a new game connection to the server and says HELLO under a throwaway name, so holding or repeatingrused to open one connection per press and inflate the server's connection counts. A second refresh right after an answer needs a second press.
Changed
- Docs and tooling: the NAT lab's player (
lab/natcheck.c) unwraps relayed datagrams withpsnr_relay_unwrap_fromand the source addressrecvfromreported, instead ofpsnr_relay_unwrap, which does not check the source and whichclient/psnr.hsays titles should not use on the P2P socket. The lab'ssymmetricscenario is the one that sends pings through the relay. - Docs and tooling:
docs/releasing.mdno longer says the ten-asset release and the first run of the app-archive upload andrelease-check.ymlare still to come: v0.5.0 is that release, and both ran on it and passed. The Windows and arm64 app binaries are still listed as not run. - Docs and tooling:
scripts/bump-version.shnow also sets thevar versiondefault of the desktop app (app/cmd/re-psn-app/main.go), andTestVersionAgreeschecks it againstVERSION; before, a bump left the app's source default at the old number. Release builds set the app version with-ldflagsand were not affected. - Docs and tooling: the website, the README images, the favicon, the README badges, the desktop app's dark colours and the status page's dark colours now follow the colours of https://undecuple.github.io/me/ (
docs/art/palette.json); the website has one dark theme (the System/Light/Dark switch and thethemecommand are gone) and a pixel-letter banner in a double frame above the title bar. The light colours of the app and the status page are unchanged. The colours credit guts.nvim through that page; guts.nvim has no licence file, so NOTICE.md names it as the source and states no licence.docs/img/social.pngmust be uploaded to the repository settings by hand to change the social preview. The banner reads "re:psn" in pixel letters and sits at the top of the page, above the terminal view; on screens at least 1400 px wide a tall copy of it stands at the left edge instead. - Docs and tooling: the features list says
since0.5.0, notmain, for the twelve rows that shipped in v0.5.0 (LAN discovery in the server, the C client and the app, game names, verified player counts, server state and reported latency in the status page,-relay-force, peer keys,-lan-public-addr, the app Server tab and the app archives), anddocs/releasing.mdhas the step that sets this column at each release. - Docs and tooling: the README settings table now lists all 35 flags (it lacked
-admin,-admin-read-token-file,-admin-tls-cert,-admin-tls-key,-discover,-name,-lan-public-addr,-medius,-medius-portand-medius-addr, and said 21), and the Flags table indocs/running.mdlists-max-player-rooms,-max-conns,-max-conns-per-addrand-max-req-rate. - Docs and tooling:
docs/side-service.mdsays re-psn offers none of the platform social services (sign-in ticket, friends, presence, block list, recent players, messages, lookup by online ID, account region and age) and that a title with its own server answers them locally; the planned friends, presence and invites feature is not built. - Docs and tooling: README no longer says the desktop app has no release binary: from v0.5.0 a release carries the app archives (the "Release archives" section already said so).
- Docs and tooling: README and ROADMAP no longer call ps3recomp#200 a draft: GitHub shows it merged into ps3recomp's
masteron 2026-10-07. - Client: The C client's writes give up after
PSNR_SEND_TIMEOUT_MSin total instead of waiting without a limit. The connection is then dead withPSNR_DEAD_SEND_TIMEOUT, as a failed write already was.
Files
Each archive holds one self-contained server binary (no installer, no runtime needed), LICENSE, NOTICE.md and an examples folder (settings file, systemd unit, proxy configs).
The re-psn-app archives hold the desktop app (one static binary, LICENSE, NOTICE.md and its README) for the same three platforms. install.sh --app and install.ps1 -App install the app from a release instead of the server; or unpack one by hand.
The x86_64 AppImage holds the same Linux binary and examples as one runnable file; it needs FUSE to mount, or run it with --appimage-extract-and-run. It is not signed.
install.sh (Linux) and install.ps1 (Windows) are the install scripts from this tag, listed in SHA256SUMS too: download one with SHA256SUMS, check it, read it, then run it (see docs/installing.md).
Check a download with sha256sum -c SHA256SUMS --ignore-missing.
96fb6ba9c92de8f79a5f32fd96dba7b57f2e2604f1334ac70dbc25e73df25d35 re-psn-app-v0.6.0-windows-amd64.zip
b2c87332043ef900a8d39bdcff50223cd3a832e8c376aba8464623007be66704 re-psn-v0.6.0-windows-amd64.zip
2f23e1abb967f00a5e681668f766ae14da677a0a906735abc45f7f561c550db2 re-psn-app-v0.6.0-linux-amd64.tar.gz
671c321827daa01575f1b7feb10fd578700f705692b9a1236e32dd19ecc8ccac re-psn-app-v0.6.0-linux-arm64.tar.gz
66b2d350297bf5dfd01bc7139e159459d3ce9cff5aef4ba8d1b3254e2193ecf8 re-psn-v0.6.0-linux-amd64.tar.gz
35bb241fa4b70ebddc686ec1d3b902b6e3f8645d965c1d64b2f1a4a1b310f8d0 re-psn-v0.6.0-linux-arm64.tar.gz
6e8c0a6fb0026bcb3eab8a6652bbc14a79f469a10f5af2998bd482a0dbbe20c3 install.sh
feb30e6c91c41b62573a10a6f87b2485eb1407b4739132b392064a8e49978b81 install.ps1
6f2f707b491534d070c5215f4c347c17975819d3237043057f17b66b6d56fec3 re-psn-v0.6.0-x86_64.AppImage
Developed with AI agent assistance.
Next release: when ready