Repository navigation
Releases: lapisproject-dev/Lapis-Net
Release list
v0.12.0: TCP hole punching (DCUtR), mail acceptance in the browser node, Kademlia routing table fix, throttled Madli observer weight, reply route for call signals
Feature and fix release containing five waves (V0.9.16 to V0.9.20): a production fix in the DHT routing table, mail acceptance in the browser node, a throttled Madli observer weight, a reply route for call signals and TCP hole punching. Hole punching is verified on Linux loopback only; whether it works behind real home routers or CGNAT is not known. The two tests that v0.11.0 named as unstable are fixed at their root cause.
Fix: Kademlia routing table (V0.9.16).
- Nabu's Kademlia engine kept only 2 peers per bucket, so a third peer of a bucket was never routable. This is a production defect, not a test artefact: bootstrap, the directory DHT and the outbox ran on heavily thinned routing tables.
LapisKademliaEnginenow keeps its own routing table with 20 peers per bucket (DhtLimits.routingBucketSize) and a replacement cache of 8. - This was the cause of the unstable
MultiNodeIpnsValueTest; the second unstable test,CallManagerBrowserIntegrationTest(a rejected call), was a missing-record race, see the reply route below. - A call signal that cannot be sent is now logged at
warnand counted; before, it was swallowed at debug level.
Mail acceptance in the browser node (V0.9.17).
- The mail acceptance check is installed in the browser node. The gossip validator and the DHT fetcher use the same check, so there is no bypass through the DHT.
- Options:
--mail-veritas-gate,--mail-karma-min-score,--mail-min-deposit-msat(environmentLAPISNET_MAIL_*, file keysmailAcceptance.*). All gates are off by default. Accepted contacts are always let through; so is every recipient of an outgoing mail. - Blocked mail is discarded without a reply and without quarantine, so a legitimate first contact is lost while a gate is on. The accepted-contacts list is in memory only: after a restart, active gates also block known correspondents until they are marked again.
--mail-min-deposit-msathas no effect yet because there is no deposit transport.
Throttled Madli observer weight (V0.9.18).
- The Veritas weight of a Madli observer was recomputed on every grant change, a full trust-graph rebuild each time. It is now recomputed at most once per 5 s window, stale-while-revalidate, off the caller's thread.
- Fails closed: if refreshes keep failing, the weight drops to 0.0 (no reputation influence) instead of keeping a stale value that could still count a revoked grant.
- A revoked grant takes effect after at most two windows plus tick jitter plus the computation time. The window is not an operator option.
Reply route for call signals (V0.9.19).
- When the callee has no directory record of the caller yet, ACCEPT, REJECT and HANGUP were lost and the caller rang until the timeout. They are now answered over the INVITE's still-open connection, using an opaque in-memory token (at most 256 routes, 4 per identity, 4 hours). The peer ID is only a routing target, never evidence of identity; no wire format changes.
- If that connection is gone, the signal is lost as before. There is no DHT lookup fallback.
TCP hole punching with DCUtR (V0.9.20).
- Two nodes behind NAT that are connected through a relay try to open a direct TCP connection (
/libp2p/dcutr). After a verified success the relayed connection is closed after a 10 s grace period (streams still open on it are cut); a failed attempt leaves it as it was. - Option
--hole-punching=auto|on|off, defaultauto: active only when--reachability=private.onaborts the start ifSO_REUSEPORTis missing. The status panel reports it asholePunching. - Verified only on Linux loopback. Not verified: the success rate behind real home routers or CGNAT. Symmetric NAT will not work. macOS is untested, Windows is not supported (no
SO_REUSEPORT). There is no UDP/QUIC punching and no AutoNAT. It needs a relay that answers Identify. - The listener port is shared with other processes of the same OS user (
SO_REUSEPORT), which is why the transport is used only when hole punching is active.
Known limits.
- The concurrency pools of hole punching are per node, not per relay: an attacker with many identities can hold all permits, and legitimate punches then fail until they time out (the connection stays relayed).
- The role registry of hole punching can be blocked for an address by a hostile peer (four foreign entries); the honest punch then fails.
- DHT lookups reveal to the peers that are asked who is looking for whom, until traffic anonymisation exists (section 11 of docs/architecture.adoc). The DHT fallback is on by default; switch it off with
--dht-fallback=false. - The acceptance gates for DM, calls and mail are off by default; the Lightning deposit has no transport.
- The accepted-contacts set, the outbox and the relay usage ledger are held in memory only.
- The downloads are not signed. The new waves had only the pipeline's own AI-assisted review and security loops, no independent professional audit.
Downloads. lapis-net-browser-0.12.0.zip and lapis-net-cli-0.12.0.zip need a JDK 25 on the PATH or in JAVA_HOME; unpack and run bin/lapis-net-browser or bin/lapis-net-cli. SHA256SUMS lists the checksum of each file. The artifacts are not signed: the checksums only detect corrupted downloads, not a compromised release account. See https://github.com/lapisproject-dev/Lapis-Net/blob/master/docs/releasing.adoc for how to verify them.
v0.11.0: relay reliability metric, DHT fallback, call remote control, wiring into browser and headless node
Feature release: a relay reliability metric with automatic relay selection, a DHT fallback for the peer directory, prekey bundles, mail and direct messages, DM and call acceptance gates, and a browser page that remote-controls voice calls. Since the wiring wave (V0.9.15) the browser node and a new headless CLI node switch these on behind operator options (table below). With the defaults, a node differs from v0.10.1 in three ways: the DHT fallback is on (see the metadata warning), the browser publishes its own record and prekey bundle again whenever its addresses change, and there is a status panel and the call page.
Relay reliability and automatic relay selection (V0.9.12).
- Madli daily vector wire format v2 with an optional "relay reliability" block. v1 vectors stay byte-identical (pinned by a golden test generated on
v0.10.1); a vector without the block is always v1. Old nodes reject v2 vectors as invalid, so the publisherMadliRelayReliabilityPublisheris off by default (enabled = false) and only the explicit optionpublish-relay-reliability=trueswitches it on. - Relay usage ledger (local, never gossiped): bytes forwarded per circuit are charged to the receiving side and credited to the relay. A peer with a negative balance is deferred in the top capacity band (
priorityHeadroomFraction, default 0.25); reputation is never a hard gate. - Automatic relay selection (
LapisNode.startAutoRelay, two relays per node) is active only when the operator declaresRelayConfig.reachability = PRIVATE. The reachability heuristic only warns on a mismatch; it never changes behaviour. The IP classifier is hardened and non-public candidate addresses are refused by default. - Wired (V0.9.15): with
reachability=private(andauto-relaynot set tofalse) the browser andlapisnet nodestart the selector, ranked by Madli reliability weighted by the local Veritas graph; the node's own record is re-published as soon as the/p2p-circuitaddresses exist. The record never advertisesRELAY: this node does not serve as a relay. The publisher is off by default and is switched on only bypublish-relay-reliability=true(it then publishes the previous UTC day once per day); nothing writes v2 vectors unless an operator asks for it.
DHT fallback for the directory (V0.9.13) and for mail and direct messages (V0.9.14).
- Hybrid: GossipSub stays the fast path and its validators are unchanged. Peer records and prekey bundles are additionally published as one IPNS value per identity, served by an own IPNS
PUT_VALUE/GET_VALUEresponder and validator (Nabu's cannot serve Ed25519 names) with a bounded record store. - Mail and direct-message pointers are left in an outbox inside that value. A rendezvous provider record lets a recipient find senders that have something for it, and a fetcher applies the same acceptance checks as the gossip path before it fetches the body. Plaintext mail deliberately leaves no outbox pointer, only encrypted mail does, so plaintext mail is never recoverable from the DHT.
- Wired (V0.9.15): the browser publishes its record, prekey bundle and outbox to the DHT (at most every 5 minutes, and with a reduced bundle because the value must stay small), hands the outbox to the mail and DM senders, runs
discoverInbox,refreshStaleand a catch-up over its accepted contacts every minute (the library throttles apply) and starts the mail fetcher. The pointer of an offline DM from the DHT passesMailboxGossip.dhtConsumer(), i.e. the same acceptance as the gossip validator;registerOutboxConsumerrefuses a second consumer, so there is no second, unchecked entrance.lapisnet nodepublishes only a peer record without any capability (no DM, no mail in the CLI).--dht-fallback=falseturns all of it off.
Call remote control in the browser (V0.8.7b).
/call.htmlremote-controls a call whose audio runs inside the node's JVM: incoming-call card (Decline before Accept), active call with timer, place-call form, call history. State arrives over Server-Sent Events.- Guarded as a local-only page: exact
Hostcheck,OriginandSec-Fetch-Sitechecks, JSON-only bodies of at most 4 KiB, a per-process session token exchanged for an HttpOnly cookie, no CORS, at most 4 event subscribers. The call history is kept in memory only. - The acceptance gates are wired (V0.9.15, off by default): one
DmAcceptanceCheckinstance goes to bothMailboxGossip(pre-check, gossip and DHT path) andDmSessionManager(classification after decryption). A call from a sender who passes no active gate is dropped silently: the caller receives neither RINGING nor REJECT (a rejection would confirm the node is online and reward probing). The call page shows a muted "blocked" row with the reason (Veritas below threshold, Karma below threshold, or the generic acceptance rule); the reason is computed locally and never sent.POST /api/dm/<peer>/acceptnow really lifts the gates for that peer and is guarded like the call routes (loopbackHost, same-originOrigin). There is no mute.
Operator options (V0.9.15). The same keys everywhere; an invalid value in any source aborts the start (browser: exit code 2 and one line on stderr), it never falls back to a default. Priority in the browser: command-line arguments, LAPISNET_* environment variables, $LAPISNET_HOME/browser.properties (at most 64 KiB), defaults. The CLI takes arguments only.
--reachability=(reachability,LAPISNET_REACHABILITY):public,privateorundeclared; defaultundeclared; never guessed.--auto-relay=(autoRelay,LAPISNET_AUTO_RELAY):trueorfalse; defaulttrueif and only if reachability isprivate;truerequires--reachability=private.--dht-fallback=(dhtFallback,LAPISNET_DHT_FALLBACK):trueorfalse; defaulttrue.--publish-relay-reliability=(publishRelayReliability,LAPISNET_PUBLISH_RELAY_RELIABILITY):trueorfalse; defaultfalse; older nodes reject v2 vectors as invalid.--dm-veritas-gate=(dmAcceptance.veritasGate,LAPISNET_DM_VERITAS_GATE):trueorfalse; defaultfalse.--dm-karma-min-score=(dmAcceptance.karmaMinScore,LAPISNET_DM_KARMA_MIN_SCORE): a finite number >= 0; default off.--dm-min-deposit-msat=(dmAcceptance.minDepositMsat,LAPISNET_DM_MIN_DEPOSIT_MSAT): an integer >= 1; default1000000.
Unknown keys, repeated keys and bare words are errors too. lapisnet node --help and lapisnet-browser --help print the options. reachability=private with auto-relay=false declares the node private but runs no selector (and the library's reachability warning, which sees UNSPECIFIED, is then replaced by one INFO line). The node's status, including the active gates, is shown read-only on the start page (GET /api/node/status); a gate cannot be edited there.
New CLI subcommand. lapisnet node [options] runs a lean headless node: relay selection, DHT directory fallback and Madli, but no DM and no mail (the CLI has no inbox, no session store). lapisnet without arguments still runs the demo.
Provisional defaults. These choices were made without an explicit decision of the maintainers and are open to change: the DHT fallback is on by default; auto-relay follows the declared reachability; the publisher is off; all acceptance gates are off (an empty trust graph would otherwise block every stranger silently); a per-identity Karma score (not defined by lapis-net-karma) is the sum of the Veritas-weighted personalised Karma of the sender's 20 most recent posts; a missing Lightning deposit is not reported as its own block reason (the deposit is a bypass, not a gate), so every block that is not exactly one gate reads "acceptance rule".
Known limits.
- DHT lookups reveal to the peers that are asked who is looking for whom - query timing and key interest in inbox, prekey and directory keys - until traffic anonymisation (section 11 of docs/architecture.adoc) exists; switch the fallback off with
--dht-fallback=false. Pointers in the DHT make this exposure persistent for up to seven days instead of only while a gossip topic is live. - The rendezvous key of a victim can be eclipsed by throwaway peer identities (a known Kademlia weakness, not mitigated); contacts already in the directory are not affected.
- The mail acceptance (
MailAcceptanceCheck) is not wired: it behaves identically on the gossip and the DHT path (both unset), so there is no bypass, but there is no mail filter either. The accepted-contacts set, the outbox and the relay usage ledger are held in memory only, and a new identity starts with a balance of 0. - Still no hole punching (
dcutr): relayed connections stay relayed, and voice calls need a direct connection. - Test note:
MultiNodeIpnsValueTest("acknowledged counts the echoes that carry the record, not the requests that were sent") failed in about 1 of 18 runs under heavy CPU load (8 to 16 busy loops on two cores): a peer was missing from the routing table andattemptedwas 2 instead of 3. The cause is not understood, so a red run of that test alone is not necessarily a regression. - The downloads are not signed. The new waves had only the pipeline's own review and security loops, no independent audit.
Downloads. lapis-net-browser-0.11.0.zip and lapis-net-cli-0.11.0.zip need a JDK 25 on the PATH or in JAVA_HOME; unpack and run bin/lapis-net-browser or bin/lapis-net-cli. SHA256SUMS lists the checksum of each file. The artifacts are not signed: the checksums only detect corrupted downloads, not a compromised release account. See https://github.com/lapisproject-dev/Lapis-Net/blob/master/docs/releasing.adoc for how to verify them.
v0.10.1 - Relay circuit-setup race fix and release pipeline
Fix release: a race in the relay's circuit setup, plus the release pipeline that builds and publishes this release's downloadable files. No new user-facing features.
Please update if you use relays. Reaching a node through a Circuit Relay v2 relay could hang for 10 seconds and then fail, without a warning or error in the log (the decoding exception showed up only in DEBUG output). The relay wrote its OK and only then rewired the stream, on another thread, so the first handshake bytes of the connecting side could arrive in between and were silently dropped. The defect is also present in v0.9.9 and v0.10.0. With the test run limited to two CPUs, plain relay connections failed in 8 of 80 attempts without the fix and in 0 of 80 with it; the same race exists in the opposite direction on the destination's leg and is fixed as well.
- Relay: the
OKand the wiring of the hop stream are one step on the stream's own event loop; on the destination's leg the stream becomes a raw pipe in the same read that delivers theOK, and early bytes are held (at most 64 KiB) until the circuit is wired. - A regression test reproduces the hop-leg race deterministically; for the destination's leg a counter of exceptions on the relay's control streams is asserted to stay at zero.
- A stall test no longer sleeps for a fixed time and then receives the old connection back.
- Test diagnostics that are inert unless switched on (
-PlapisDiag, see CONTRIBUTING.adoc). - The slow-reader relay test from
v0.10.0is deterministic now (a manual clock instead of a real TCP race).
Release pipeline. This is the first release built by it. Pushing an annotated tag builds that exact commit, runs both distributions (smoke test), writes SHA256SUMS and publishes the release as a draft that is only published after the uploaded files were downloaded and compared. Publishing stops if the tag no longer points at the verified commit (a move within the few milliseconds around a call cannot be excluded, see docs/releasing.adoc), and it never replaces a file of a published release. Before this release the publish step was tested only against local stand-ins, not against GitHub itself. How it works: docs/releasing.adoc
Known limits. If the destination sends its OK and the first bytes of its end-to-end handshake in a single read, those bytes can still be lost; this is not covered by a test and has not been observed. A control stream that raises a decode exception is still left open. The downloads are not signed. The GitHub release for v0.10.0 has no files and is not back-filled, because it contains the race.
Downloads. lapis-net-browser-0.10.1.zip and lapis-net-cli-0.10.1.zip need a JDK 25 on the PATH or in JAVA_HOME; unpack and run bin/lapis-net-browser or bin/lapis-net-cli. SHA256SUMS lists the checksum of each file. The artifacts are not signed: the checksums only detect corrupted downloads, not a compromised release account. See https://github.com/lapisproject-dev/Lapis-Net/blob/master/docs/releasing.adoc for how to verify them.
v0.10.0 - Hardening the relay and the storage layer
Maintenance release: hardening of the relay and of the DHT storage layer (designed as waves V0.9.10 and V0.9.11). First release whose version number simply counts releases in the order they ship; see "Releases and version numbers" in docs/roadmap.adoc.
Please update: memory-exhaustion bug in the lookup layer
Any peer connected to a node could send a crafted Kademlia lookup request that made the node try to allocate up to about 2 GiB per request on its network thread. The defect existed since v0.1.0 and was exposed by the browser node. Requests are now validated before anything is allocated.
Storage layer (V0.9.11)
- Absolute deadlines and concurrency caps for provider lookup, announcement and fetch.
- Bounded provider store (about 16 MiB at most). A peer using many identities can still crowd the eight provider slots of one content key; documented residual risk.
- Peer addresses learned from the DHT are sanitized (per-peer and per-IP caps, no DNS forms, public-only in WAN mode).
- Streams opened during lookups are closed; a Bitswap want-leak across peers is closed.
Relay (V0.9.10)
- Per-circuit stall limit (30 s) and per-connection stall budget (60 s per 10 min beyond a 1 s grace): a peer that stops reading no longer freezes the other circuits on the shared connection.
- A relayed connection the peer already opened is reused instead of dialling a second one.
Not changed / known limits
- The stall is bounded, not eliminated; the budget belongs to the connection, not to the peer that caused the stall.
- Messaging modules still use GossipSub for discovery; migration to the repaired DHT path is a separate step.
- Relays are chosen explicitly; no relay reputation yet.
- Known flaky test:
CircuitProxyBackpressureTest"the same slow reader IS cut off once the grace is switched off" (failed in 2 of 5 runs; cause not established).
No binary artifacts are attached to this release; build from the tag with ./gradlew clean check.