Repository navigation
Releases
v0.6.0
Pre-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 i...
v0.5.0
Pre-release: this is a 0.x version and may change without notice.
Added
- The desktop app's Status screen has a read-only Server tab when a status URL is set: version, uptime, relay on or off, the limits in
force (0 shown as none), TCP refusals and UDP drop counters, and the payload the relay has carried with a bytes-per-second rate
between two refreshes (not network bandwidth). A counter going down is shown as a server restart. A server that omits the fields
shows each as not reported, never as 0. It changes nothing on the server. - The release build also makes three archives of the desktop app,
re-psn-app-vX.Y.Z-linux-amd64.tar.gz,-linux-arm64.tar.gzand
-windows-amd64.zip, each with the staticre-psn-appbinary (no cgo, version set from the tag),LICENSE,NOTICE.mdand the
app's README, and listed inSHA256SUMS. A release now has ten assets;scripts/check-release.sh --assets, both release workflows
and their tests expect them (v0.4.0, with seven, is still accepted by the release check). The install scripts do not install the app unless asked (see below). install.sh --appandinstall.ps1 -Appinstall the desktop app (re-psn-app) from a release instead of the server, with the same checksum check, no sudo and no profile edits. They need a release that has the app archives (after v0.4.0) and say so when the download is missing. Without the flag the scripts behave as before.-lan-public-addr IP(RE_PSN_LAN_PUBLIC_ADDR, off by default, IPv4): when a player connected from a private, carrier-grade NAT or
link-local address, players on public addresses are given IP and that player's P2P port instead of the unreachable LAN address
(the host must forward that port). Players on private addresses and players with public addresses are unaffected.- LAN discovery on the server, opt-in:
-discover(RE_PSN_DISCOVER, off by default) answers a 64-byte DISCOVER sent to the game UDP
port with a 52-byte DISCOVER_REPLY (TCP and UDP ports, relay flag, player count, name, version), only to private, carrier-grade NAT,
link-local or loopback senders, under the existing UDP rate limit.-name(RE_PSN_NAME, at most 24 printable ASCII bytes) sets the
name shown./api/statsudp_dropsgainsdiscover_refused. With the flag off the server behaves as before. No client helper yet. - The C client can find servers on the local network:
psnr_discover(port, timeout_ms, out, max)(client/psnr.h) broadcasts one UDP request from its own temporary socket (never a title socket) and returns each server that answers. Only servers started with discovery on reply; it does not cross subnets or client-isolated Wi-Fi, so keep a manual host:port path. On Windows it broadcasts to 255.255.255.255 only; elsewhere it also tries each IPv4 interface's directed broadcast. /api/statsreports the server's version, whether the relay is on, the payload the relay has carried, the limits in force and
how many TCP connections were refused (too many from one address, request rate). Aggregate only; same access rules as the rest
of/api/stats. Older servers omit the fields.- HEARTBEAT may carry the player's own last round trip (
rtt_ms, 2 bytes). The server shows it next to each member on the status
page and in/api/statsas reported by the player, unverified and for display only; a HEARTBEAT without it, and older servers,
behave as before. The C client sends it once a title opts in withpsnr_report_rtt(c, 1)(recommended; off by default), from the
round trips of its own calls (psnr_rtt_ms,psnr_heartbeat). The app's Go client does the same withConn.ReportRTT(true). /api/statsand the status page count verified players: each public room hasverified(members whose UDP probe the server
has accepted on this connection) and the top level hasverified_players. The page shows "(N verified)" after a room's
member count when N is above zero. The protocol is unchanged.client/psnr.hnow says what the C client touches: only the sockets it makes itself (the server connection and relay stream connections, which the caller owns), never the title's; connecting and disconnecting are explicit and nothing reconnects by itself; the process-wide effects (WSAStartupon Windows, one global connect-error value, plainsend()so a closed connection can raiseSIGPIPE, IPv4 only). A comment only; no code changes.- A test checks that no secret reaches the log: it drives a session at debug level, in both log formats, and looks for each player's UDP token, a room's invite code (right and wrong), a peer key and the status token (right and wrong). The server's behaviour is unchanged.
- Peer keys, for players who connect directly: a client may send PEER_AUTH (0x03, after HELLO, when HELLO_ACK's flag bit 2 says the server issues keys), and then each pair of members who both asked gets one new random 16-byte key from the server in a PEER_KEY push (0xA8) when they share a room. It is the groundwork for tagging packets between peers; no client uses it yet. A client that does not ask is unaffected, and the keys are not confidential on the wire (they travel over the plain TCP connection). HELLO_ACK's flags gain bit 2.
-relay-force(RE_PSN_RELAY_FORCE, off by default, needs-relay): every stream goes through the relay, even between players
who could connect directly, so the relay can be tested from one network. The server logs a warning at start and refuses to
start with it and without-relay. It sets the existing ROUTE flag, so clients need no change; datagrams (UDP RELAY) are not
forced. For testing only: it uses the host's bandwidth.- The desktop app can find servers on the local network: Find on LAN (
fin Settings, Ctrl+F while typing the server, also on the
first start) broadcasts one 64-byte UDP datagram (PSNR, type 0x04, a random nonce) from a temporary socket, lists the replies
(name, address:port, players, relay, version) and puts the chosen address in the server field; typinghost:portstill works.
The Go client package hasDiscoverfor it. A server answers only when it was started with-discover, and only inside its own subnet; otherwise the list says none found.
Added
- The status page and
/api/statsshow known games by name: rooms and leaderboards carry alabelsuch as
[Killzone 3 (BCUS98234, BCES01007)], withgameandserialsfields, and are sorted by it. A game the table does not
know keeps its communication ID. The table is embedded in the server (four games so far), never fetched, and the
titlefield is unchanged. The protocol is unchanged.
Fixed
deploy/re-psn.env.examplenow listsRE_PSN_DISCOVER,RE_PSN_NAMEandRE_PSN_LAN_PUBLIC_ADDR, and the old names of those and of-relay-force, soscripts/check-deploy.sh(run by the release build) passes again. A test now runs that check, so a flag added without its example line fails in CI.- The start log line for LAN discovery ("lan discovery on") no longer shows the server name quoted twice; the name is logged as a plain
attribute, quoted once by the log format. A test checks it in text and JSON. - The advice for the UDP PROBE (
client/psnr.h,docs/api.md) is now concrete: send one about every 15 seconds while the title is
online, from its own P2P socket, because some home gateways drop an idle UDP mapping after 30 seconds. Comments and docs only. re-psn -hno longer prints the status token. The-http-tokendefault was read fromRE_PSN_HTTP_TOKEN(or the old
PSNR_HTTP_TOKEN), so the usage text showed it, and a pasted-hor a CI log could leak it. The flag's own default is now
empty, as for-db, and the variable is read after the command line is parsed. The token is still taken from either
variable or the flag, the flag still wins, and nothing else about the status page changes. If you ever ran-hwith the
token set and kept the output, rotate the token.
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.
cb3460d041455f4c23cc46b813317c4b9dff21bb54373adbfe93e4e487dccb1e re-psn-app-v0.5.0-windows-amd64.zip
cd2c8c526aa451f6aeb68beae3345f84dd12ca35ee926f546843b93dca79b907 re-psn-v0.5.0-windows-amd64.zip
1757d27f014e4caf06f5090a9ca632a7840b825d37c46c010b329f3e4dcfa909 re-psn-app-v0.5.0-linux-amd64.tar.gz
d6b69a93b8e1cb7c2e627f2c72d50175b4c0c64f585516cb6cdf88a9676167ab re-psn-app-v0.5.0-linux-arm64.tar.gz
be5421bd560216f3464611d139209a9d19c934c48c26c38404c3f5370a6c9f8c re-psn-v0.5.0-linux-amd64.tar.gz
5ea9210696804237d993c5d20bb9289d85eb8621aa83f14c6593ccd2060e08b9 re-psn-v0.5.0-linux-arm64.tar.gz
6e8c0a6fb0026bcb3eab8a6652bbc14a79f469a10f5af2998bd482a0dbbe20c3 install.sh
feb30e6c91c41b62573a10a6f87b2485eb1407b4739132b392064a8e49978b81 install.ps1
f14c4a10bc5fa4135d248e31a71dd465a3037874254c53b7cdc71765561be318 re-psn-v0.5.0-x86_64.AppImage
Developed with AI agent assistance.
Next release: when ready
v0.4.0
Pre-release: this is a 0.x version and may change without notice.
Added
- Optional fail2ban instructions and a disabled journal jail/filter for refused HTTP status tokens, with offline
synthetic log checks. Live host bans are unverified; the example leaves game TCP/UDP outside its scope. - A recomp developer integration guide with pinned runtime sources, callback/lifecycle and peer-transport guidance.
A minimal C HELLO/search example runs in the required native-client check; C API comments now clarify blocking
writes, diagnostic state and reply-wait timeout limits without changing implementation or the wire protocol. - Native optional HTTP status in the shared Windows/Linux terminal app: server totals, public-room members/age and
leaderboard summaries, using a local Bearer-token file. Requests and displayed data are bounded, redirects/proxies
are disabled and failures fall back to the game protocol. Initial refresh and changed-source sequencing are corrected. - Linux desktop startup for the shared terminal app: open an installed terminal when no TTY is attached, with literal
arguments and a recursion guard. A desktop entry and the site's favicon artwork are provided as installation inputs;
no release assets are added, and the Windows build retains its normal console behavior. - Bounded per-source UDP admission (per IPv4 address, per IPv6 /64) with
-max-udp-rate/RE_PSN_MAX_UDP_RATE(default 1000 packets/second,
same-size burst; 0 disables it), shared by probes and relay datagrams. Aggregateudp_dropscounters in the
opt-in/api/statsidentify refusal reasons without per-packet logging or source labels. Relay byte/stream budgets
remain planned; the packet format and token checks are unchanged. /healthzon the opt-in status listener, protected by the existing HTTP token. It reports game TCP/UDP readiness
without player data. Shutdown marks the server unready and closes accepted client/relay sockets with bounded cleanup;
restart behavior and supervisor settings are documented indocs/restarts.md.- A stock Caddy 2 example for the HTTP status page, included with the deployment examples. It defaults to loopback;
game TCP and UDP stay direct. The example has not been run or validated with Caddy. - The opt-in server status page now matches the website and terminal app's gruvbox-material palette and monospace
framing, with Overview/Rooms/Leaderboards tabs, System/Light/Dark, text size, spacing and contrast-checked custom colours.
Appearance is local to each browser. Live refresh can be paused and retains the last snapshot on failure; without
JavaScript both tables remain visible and reload as before. The status token is unchanged; the fields of/api/stats
are unchanged by this page (theudp_dropscounters are a separate addition).
Embedded assets normalize line endings before rendering and CSP hashing. Polling preserves full 64-bit score and
identifier values without changing their JSON types on the server. - An offline repository check for tracked root/top-level layout, local Markdown links and anchors, and static
workflow/Dependabot input paths. Runpython3 tools/repo_check.py; CI runs it with the documentation checks. - A focused loopback check:
python3 tools/loopback.pyruns the existing native C-client and Go app-client conformance
tests against temporary loopback servers. Required skips and timeouts fail;--raceis optional. Seedocs/testing.md. - Numbered file-style output in the website's terminal view: a header, frame, line-number gutter, syntax-style colours
and wrapped continuation rows with an empty gutter. Headers and numbers are excluded from text selection. See
docs/website.mdfor controls and browser-test instructions. - Optional install scripts:
scripts/install.sh(POSIX sh; Linux amd64 and arm64) andscripts/install.ps1(Windows amd64;
written for PowerShell 5.1 and 7, tested on 7 only) install the server for the current user from a GitHub release, the newest one (pre-releases included)
or a pinned--version/-Version. They check the download againstSHA256SUMSand stop before installing anything on a
mismatch or a missing line, need no sudo or administrator rights, edit no shell profile, and change the user PATH only with
-AddToPathon Windows. macOS is refused (there are no macOS builds).--dry-run/-DryRundownloads nothing. Tested
withscripts/test-install.shandscripts/test-install.ps1against a fake release. Seedocs/installing.md. .github/FUNDING.yml, which gives the repository a sponsor link (Ko-fi).- Tests for room kick behavior, contributed by Tzeentchnet.
Changed
- The website's pager trial is removed: the
pagercommand, its keys, buttons and status bar, and the saved pager
setting are gone, and long output is shown in full in the scrolling output pane. Numbered file output is unchanged. - The README install section recommends the release archives and shows an optional quick install: the installer is
fetched from a pinned revision of the repository, its hash is checked, and it is read before it is run. - The release carries seven assets: the three archives, the AppImage,
SHA256SUMS, and the two install scripts as
install.shandinstall.ps1, byte for byte the taggedscripts/files, with their checksums inSHA256SUMS. Check one
withsha256sum -c SHA256SUMS --ignore-missingbefore reading and running it.scripts/check-release.sh --assetschecks a
build holds exactly the seven, andrelease-checkcompares the published scripts with the tag.
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 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.
c9e8a56940d8e828b0e8a096eac9135c8a0cd8057976756acf43fd5e45297a53 re-psn-v0.4.0-windows-amd64.zip
86bc5678b79f089b7bdabe284d562df85c85c0964b7dedfd941a29e9c57f6e8b re-psn-v0.4.0-linux-amd64.tar.gz
90766c21d90468741105800ad1687f0e33d4c742f60f04c7144eac2404498ef0 re-psn-v0.4.0-linux-arm64.tar.gz
fef1f7f9a424275e45cbca80398477b4440c82b87c106ce6aa7626f3bbce1772 install.sh
98ab633eeeffaecc6968e2fa342830ad4ceed4a5d2397a19dfcfb28e16d9d3a7 install.ps1
acb0bf09dc5f5df1259e612618e3f39b6e3d497ef70fe4e219666ec7796ea328 re-psn-v0.4.0-x86_64.AppImage
Developed with AI agent assistance.
Next release: when ready
v0.3.0
Pre-release: this is a 0.x version and may change without notice.
Changed
- The website starts in the plain view on a phone (narrow screens); the
terminal viewbutton switches the interface on, and nothing is stored. - The website's interface on a phone: the version no longer wraps one letter per line in the title bar, and the menu is a row of tabs that scrolls sideways with the page content shown under it, instead of a full-screen list that hid the content.
- Credits: README, NOTICE.md and the website name undecuple as the maintainer of re-psn and author of the changes since the fork, and LICENSE
keeps upstream's copyright line with a second line for the fork's changes. - The status page and
/api/statsare off unless-http/RE_PSN_HTTPnames an address; they used to listen on
127.0.0.1:36101by default. To keep the old behaviour, setRE_PSN_HTTP=127.0.0.1:36101(or-http 127.0.0.1:36101).
-http-tokenand the warning for a page reachable beyond localhost without a token are unchanged. - The release carries one
SHA256SUMSinstead of a.sha256file per download: five assets instead of nine.
Check a download withsha256sum -c SHA256SUMS --ignore-missing. - A shorter README: its long reference parts fold into dropdowns, and a short list linking to
docs/features.mdreplaces the features table. - The container image moves from Alpine 3.20 (end of life) to Alpine 3.24 and Go 1.26, both bases pinned by digest, and the server
runs as the non-root user 10001. Tested with Podman and in the NAT lab on CI.
Added
re-psn-appstarts a terminal UI when run with no arguments, built on Bubble Tea v2.1.1 and Lip Gloss v2.0.6 (MIT; the app module's
first dependencies, pinned inapp/go.modandapp/go.sum; the server module still has none). On first start it asks whether
this machine plays, hosts or both, then for the online ID and the server, and savesre-psn.ini. The home screen has a menu
(Overview, Rooms, Leaderboards, Settings, Help) beside the content, or one at a time under 70 columns. Rooms and Leaderboards
are the tabs of a Status screen filled over the game protocol only (HELLO, SEARCH_ROOMS, GET_RANKING for a typed board id);
the server's version, uptime and connection counts read "not available over the protocol". Settings edits mode, name, server,
status URL and theme ([app] theme, new) and saves with mode 0600. Dark by default,tfor light, in the website's colours;
NO_COLORturns colour off. Without a terminal, or with--no-tui, it prints one line and exits 2. Seeapp/README.md.- Website: autocomplete in the terminal interface. While you type, the command line shows the rest of the best match dimmed and a list of up to eight matches (command names,
--statusand its values, feature ids, setting names,themevalues) with a one-line description each. Tab or Right arrow takes the match, Up and Down choose from the list, Enter takes a chosen item, Esc closes the list, and a click or tap takes an item. The input is an ARIA combobox and the list a listbox, with a polite count of the suggestions. Nothing new is requested or stored. - Website: a visible System / Light / Dark switch in the page header and in the terminal interface (without script it is hidden and the page follows the system), kept in step with the
themecommand and remembered in the browser; and colour from the whole gruvbox-material palette for headings, status tags, command names, the prompt, flags, ids, links and the bars. Every text colour is checked at 4.5:1 or more against its background in both modes; where an accent is too faint as text on the light page it is a border or an underline and the text keeps a passing colour. site/, the project website:tools/site.pygeneratessite/index.htmlandsite/data.jsonfromdocs/features.tsv,VERSION, the README settings table
and this file (--checkcompares the committed files and the colour tokens ofsite/style.csswith the sources), andsite/style.cssis hand-written in the
gruvbox-material palette, dark by default and light with the system. No script and no third-party loads (a CSP says so)..github/workflows/pages.yml
deployssite/to GitHub Pages on a push to main that touches it, or by hand; the deploy job runs only when the repository variablePAGES_ENABLEDistrue.site/terminal.js, the terminal-style interface of the project website: a menu, a content pane and a command line (features [--status S] [id],roadmap,settings [name],
install,connect,version,changelog,about,credits,licence,docs,links,theme,clear,history,--help), Tab completion, history, a dark and a light theme,
and, on a phone, a row of scrolling tabs over the content with tappable command chips. The page still works without script and aplain viewlink switches the interface off. The only request is fordata.json
on the same site (the content security policy allows scripts and requests from this site only). The section bar now stays in view while scrolling, there is a Top link, a real
page heading and a favicon. Tested withnode --test tools/terminal.test.js.app/, the start of the desktop app, as a Go module of its own: a client
for the TCP protocol (HELLO, heartbeat, rooms, leaderboards, and the MEMBER_JOINED, MEMBER_LEFT and ROUTE pushes) with a
conformance test against the real server, a reader and writer for the per-user settings filere-psn.ini, and
re-psn-app check HOST:PORTandre-psn-app version, next to the terminal UI described above. Seeapp/README.md.docker/docker-compose.test.yml: an opt-in hardened host-network compose file for a shared test host (non-root, no capabilities,
read-only filesystem, resource limits). It refuses to start without an explicitRE_PSN_BINDaddress..github/workflows/container.yml: builds the image for linux/amd64 and linux/arm64 and, when a release is published, pushes it to
ghcr.io/undecuple/re-psn(tagsX.Y.Z, plusX.Yandlatestfor a release that is not a pre-release). A manual run builds and
pushes nothing unless asked. The first image,0.2.0, was pushed by a manual run (amd64 and arm64, unsigned); the arm64 image was not run.-replace(RE_PSN_REPLACE): when the game port is held by an earlier re-psn of the same user, the server sends it SIGTERM,
waits up to 10 s for the port and starts in its place (Linux only). It finds the earlier instance through a pidfile in
$XDG_RUNTIME_DIR/re-psn/and signals it only if the pid, owner, start time and program name all check out. The server now
stops cleanly on SIGINT or SIGTERM, and without-replacea busy port names the earlier re-psn holding it. Seedocs/running.md.
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 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.
Check a download with sha256sum -c SHA256SUMS --ignore-missing.
b8768bebab9ae83e2bf264cace021c855eb1e5bc7620cc120948a961e484bd75 re-psn-v0.3.0-windows-amd64.zip
2003d6e6b6c2f1272d6aa2abf0a9891ce89ee13b55a72883fec236ea29b06df5 re-psn-v0.3.0-linux-amd64.tar.gz
29612108c75e942ef3a5e5f039cd9e727c93932b57a92ff1e1326204226c727e re-psn-v0.3.0-linux-arm64.tar.gz
ff4bb0a0748f74d8da7e2aedb98a2b1de5414ec5d4cbb8b929690c52f4dd7b86 re-psn-v0.3.0-x86_64.AppImage
Developed with AI agent assistance.
Next release: when ready
v0.2.0
Pre-release: this is a 0.x version and may change without notice.
Added
-
A Linux x86_64 AppImage of the server in the release,
re-psn-vX.Y.Z-x86_64.AppImage, beside the archives: the same binary and
examples as one runnable file, built byscripts/build-appimage.shwith appimagetool 1.9.1 and the type-2 runtime 20251108
(both downloaded by the release workflow and checked against pinned SHA-256 hashes), smoke-tested in the workflow, listed in
SHA256SUMS. Unsigned; needs FUSE or--appimage-extract-and-run. Seedocs/releasing.md. -
docs/img/social.png(1280x640), drawn by the art generator for the repository's social preview. It is uploaded by hand in the
repository settings and no page uses it. -
tools/art/make_art.pydraws the README banner and terminal images (docs/img/) in the gruvbox-material palette
(docs/art/palette.json) with JetBrains Mono pinned by SHA-256. Terminal images come only from transcripts of real runs
(docs/art/transcripts/, recorded bytools/art/capture.sh). CI renders them again and fails on any byte that differs. -
AGENTS.md: the commands, layout and rules for AI coding agents working in the repository. -
A
VERSIONfile with a test that the code, this file and the README agree on it,scripts/bump-version.sh(one bump, every place,
--dry-runshows the diff),scripts/release-notes.sh(notes from this file) and
scripts/check-release.sh, run byscripts/release-build.sh. Seedocs/releasing.md. -
Scores go through a storage interface, and
-db/-db-file(RE_PSN_DB,RE_PSN_DB_FILE) pick the backend. Onlyoff(the default)
andmemoryexist, so scores are still kept in memory; database settings stop the server at start, and the setting is never printed. -
docs/connecting.md: the ways players reach a server (LAN, port forwarding, a public server with-relay, Tailscale, ZeroTier, a
reverse proxy), with diagrams and links to the overlays' documentation. re-psn has not been run over Tailscale or ZeroTier. -
A features list:
docs/features.tsvholds one row per feature with its status and evidence, andtools/features.pygenerates
docs/features.mdfrom it. CI runspython3 tools/features.py --check, which fails on a stale page, a malformed row, or
evidence that does not exist. -
Release archives include an
examples/folder:re-psn.env.examplewith every setting, a systemd unit, and Traefik, nginx and HAProxy
configs that pass the real client address.scripts/check-deploy.shchecks the env file against the server's flags and runs in
scripts/release-build.sh. Only the Traefik config has been run. -
Reverse proxy support:
-trusted-proxiesand-proxy-protocol(PROXY protocol v1/v2 on the game TCP port), and forwarded
headers for the status page log, both off by default. The real client address then counts for-max-conns-per-addr, the
name take-over rule,public_ipand the log. UDP is not proxied. Seedocs/running.md. -
Every flag can be set as a
PSNR_<FLAG>environment variable (a flag wins), and-tcp-addr/-udp-addrgive the two
game sockets their own addresses or ports. A bad value stops the server at start. Seedocs/running.md. -
Invite-only rooms (protocol 0.4):
SET_ROOM_ACCESS,JOIN_BY_CODEandGET_ROOM_ACCESSgive a room an 8-character code and take it
out of search and the status page (which counts "unlisted rooms"). Wrong codes are rate-limited. HELLO_ACK flags bit 1 says the server
has them. Seedocs/api.md. -
Logging with levels:
-log-level(debug, info, warn, error),-log-format(text or json) and-log-file. Every refusal
(connection or request limit, name taken, status page token) is now logged at warn level;-vis the same as
-log-level debug. Tokens and query strings are never logged. Seedocs/running.md. -
One player per name on a server, compared case-insensitively. A HELLO with a
name someone else holds gets ERROR 8 (name taken); from the same IP address
it is the same player reconnecting, and the old connection is closed. The C
client reports the refusal throughpsnr_connect_error(). -
Firewall:
Setup.cmdoffers to add a Windows Firewall rule for the server
(TCP 36100, one UAC prompt);setup.shprints the ufw/firewalld command when
a firewall is on.docs/running.mdcovers the server's and players' rules. -
NAT traversal. The server listens on UDP 36100 too. A player's P2P socket
sends it a PROBE (with a token from HELLO_ACK), and the server records the
public endpoint the probe came from. Member entries now give each viewer the
address that works from where they are: that public endpoint from another
network, the member's LAN address from the same one. Clients built against
0.2 still work; without a probe nothing changes. The C client builds and
reads the packets (psnr_probe_packet,psnr_punch_packet,
psnr_probe_reply,psnr_is_control); the caller sends them from its own
P2P socket. Protocol indocs/api.md, "UDP". -
The relay (
-relay, off by default), for players who can't reach each other
directly. Streams into a player behind a router go through the server
(STREAM_CONNECT / STREAM_OFFER / STREAM_ACCEPT / STREAM_READY), and ROUTE
pushes say which members need it. Datagrams go through the server's UDP
port (RELAY) when punching doesn't get through, as with a symmetric NAT.
HELLO_ACK gains a flags byte saying whether the relay is on. The C client
wraps and unwraps relayed datagrams and opens and accepts relayed streams.
In the lab, the symmetric-NAT scenario now passes, and every scenario also
runs a host-to-joiner stream. -
lab/: a NAT lab in Docker. A psnr server on a pretend internet, headless
players behind NATing routers, and three scenarios: two homes (passes), a
symmetric NAT (fails until psnr has a relay), and two players in one home
(passes, over LAN addresses). CI runs it.
Fixed
docs/running.mdsaid the P2P port is UDP only; titles use TCP on it too.
Changed
- The release workflow builds its notes with
scripts/release-notes.shfrom the CHANGELOG.md entries for the version inVERSION
(ending "Next release: when ready"), and on a tag fails before building unless the tag isv+VERSIONand
scripts/check-release.sh --tagpasses. A manual run still builds everything and publishes nothing. README.mdis rewritten: a fork notice crediting sp00nz, an overview with a session diagram, an example from real local runs,
the available features, install, a settings table checked against the flags, and thePSNR_*migration note.ROADMAP.mdis rewritten around the feature ids (Done, Now, Next, Later, Not planned), keeping upstream's scope section; the
features list gains planned and later rows for storage, high availability, metrics, the apps' themes and the docs, and
tools/features.py --checkalso fails when a roadmap item sits under a section that does not match its status.deploy/README.txtis nowdeploy/README.md(examples/README.mdin the release archives); the docs share one layout, and the deploy and Docker READMEs gain Mermaid diagrams.- The docs use the fork's name, re-psn, and credit sp00nz as the author of the original psnr.
client/psnr.ckeeps its name. - Settings are read from
RE_PSN_<FLAG>variables. The oldPSNR_<FLAG>names still work as aliases: a flag wins, then
RE_PSN_<FLAG>, thenPSNR_<FLAG>. An old name logs a warning at start naming the old and new variable (never the value), and
if both are set and differ the new one is used, with a warning.deploy/psnr.env.exampleis nowdeploy/re-psn.env.example,
with the old names in a comment block;scripts/check-deploy.shchecks both lists and refuses a livedeploy/re-psn.envor
deploy/psnr.env. The compose file, Docker README and proxy examples use the new names. - The server binary is
re-psn(re-psn.exeon Windows): release archives arere-psn-vX.Y.Z-<os>-<arch>(.tar.gz, .zip) with
re-psn-*.sha256files and SHA256SUMS, and the release workflows look for those names. Nopsnrcopy is shipped. The image, the
compose service and theSetup.cmd/setup.shlaunchers (Start re-psn.cmd,start-re-psn.sh) and Windows firewall rule
(re-psn; the oldpsnrrule is removed) use the new name.deploy/psnr.serviceis nowdeploy/re-psn.service: it starts
/usr/local/bin/re-psnand reads/etc/psnr/psnr.env, then/etc/re-psn/re-psn.env(both optional, the later one wins); the
state directory stays/var/lib/psnr.scripts/check-deploy.shchecks the unit's binary and both env paths. The Go module path,
the C API, the UDP magic and the wire format are unchanged. - The architecture and NAT lab diagrams are Mermaid (GitHub renders them), and
docs/architecture.mdgains a diagram of the optional
reverse proxy path and diagrams of a relayed connection (a VPS example) and of how a relayed stream is set up. The text there that said
there is no hole punching and no relay is corrected. - PROXY protocol v2 headers are now checked for family and transport: unassigned values, and a datagram or unspecified transport on the
TCP game port, close the connection (a LOCAL header still carries no client and is consumed to its declared length). The nginx example
is labelled v1 (proxy_protocol on;sends v1), anddocs/running.mdsays Cloudflare Spectrum's PROXY protocol is an Enterprise feature. - The Dockerfile, compose file and
Dockerfile.dockerignoremoved intodocker/, with a README. Build from the repository root with
docker build -f docker/Dockerfile ., or rundocker compose -f docker/docker-compose.yml up -d.lab/run.shand Dependabot follow. - The Docker image no longer forces
-http 0.0.0.0:36101without a token: the status page stays on localhost inside the
container untilPSNR_HTTPandPSNR_HTTP_TOKENare set (the compose file shows how). - Protocol additions that Simpsons Arcade's Matc...