The first release that changes what a client is allowed to reach and where the server keeps what it learns. A device's credential can be a key instead of a shared secret, a client can be held to the ports it may reach and not only to the names, the server's runtime state lives in a file of its own, and both clients can bring the tunnel back by themselves. No client has to be enrolled again — the handshake, the ALPN and the L4 preface are unchanged on the wire, so a 0.6.1 client talks to a 1.0.0 server and the other way round. What did change is what a deployment and a device have to do.
Pick a build below if the guess is wrong.
Client app first, then the server. Nothing needs an account, and nothing is gated.
macos — the .dmg and the .app inside it carry no
Developer ID signature and are not notarized. macOS Gatekeeper refuses them until the user
allows them by hand.windows — the NSIS installer (-setup.exe) carries no
Authenticode signature. Windows SmartScreen refuses it until the user allows it by hand.A code signature costs money — the Apple Developer Program is $99 a year and a Windows certificate is bought per year on top of that — and this project has no revenue, so the first release ships the artifacts without one. Everything else is signed: the Android APK carries the release keystore, the server archives carry a checksum, and the container image is signed with the release key and carries a provenance attestation.
On macOS the refusal is the strict one: the app does not open until the user chooses “Open Anyway” in the dialog, which is a click in System Settings → Privacy & Security. The Tauri updater’s own signature is separate from the operating system’s and is present either way, so an update installs the same way as the first install does.
One APK per ABI: arm64-v8a for every phone, x86_64 for the
emulator images a development machine can run. Desktop bundles are signed with the Tauri updater
key; the in-app updater reads latest.json in the same release. iOS is not a supported
platform — see the
roadmap.
Checksums for every asset above: SHA256SUMS.txt.
Each archive holds the nexapipe binary, the client cdylib
for embedding, config.toml.2fa.example, the README and the licence. Checksums for
every asset above:
SHA256SUMS.txt.
Published with everything else, as one multi-arch manifest for
linux/amd64 and linux/arm64. Both READMEs carry a
systemd unit for the server now as well.
# This release exactly docker pull ghcr.io/open-nexa/nexapipe:1.0.0 # The newest stable release (:latest never points at a pre-release) docker pull ghcr.io/open-nexa/nexapipe:latest
docker-compose.yaml names the published image, so docker compose up
pulls it rather than building — --build still builds locally under the same name.
The image runs as uid 1000, not root. The three mounted directories
have to be writable by that uid, so run sudo chown -R 1000:1000 config secrets logs
before the first start of an upgraded deployment. Without it the iroh.key cannot be
opened and is regenerated — a new Node ID, and every client stops finding the server. See
before upgrading.
No package manager yet — these are the release assets, straight from the tag.
# Linux x86_64, static binary
curl -LO https://github.com/open-nexa/nexapipe/releases/download/v1.0.0/nexapipe-1.0.0-x86_64-unknown-linux-musl.tar.gz
tar -xzf nexapipe-1.0.0-x86_64-unknown-linux-musl.tar.gz
cd nexapipe-1.0.0-x86_64-unknown-linux-musl && ./nexapipe --version
# macOS, Apple silicon
curl -LO https://github.com/open-nexa/nexapipe/releases/download/v1.0.0/nexapipe-1.0.0-aarch64-apple-darwin.tar.gz
tar -xzf nexapipe-1.0.0-aarch64-apple-darwin.tar.gz
cd nexapipe-1.0.0-aarch64-apple-darwin && ./nexapipe --version
# Linux — verify before extracting: the check looks for the archive beside # SHA256SUMS.txt, so it has to run before the cd into the directory curl -LO https://github.com/open-nexa/nexapipe/releases/download/v1.0.0/SHA256SUMS.txt sha256sum -c --ignore-missing SHA256SUMS.txt
# macOS — the same check with shasum, which is what ships there. macOS 26 # added sha256sum, but every release older than that has only shasum, and not # every version of it understands --ignore-missing, so grep narrows the list # to the one archive you actually downloaded. Still before the cd. curl -LO https://github.com/open-nexa/nexapipe/releases/download/v1.0.0/SHA256SUMS.txt grep nexapipe-1.0.0-aarch64-apple-darwin.tar.gz SHA256SUMS.txt | shasum -a 256 -c -
All assets: SHA256SUMS.txt.
1.0.0 follows 0.6.1. No configuration key is removed or renamed and every key
this cycle adds is optional, so a config.toml that works on 0.6.1 still loads —
but five things behave differently without being configured to, and all of them are listed under
before upgrading.
nexapipe client add <ID> --device <NAME> --public-key <HEX>.
A device that has no key keeps using its secret, and the two are checked the same way.allow_hosts grew a port dimension: allow_ports names the ports a
client may reach, and allow_ports_strict says whether that list is checked against
the port the request is served from or the one the client names.allow_ports out leaves a client with every port it had.config.toml,
so the configuration is not rewritten for them and a host edit cannot lose a counter.config.toml puts its state
in config.state.toml — and the old state.toml beside it is still
read when the new one is not there yet.relay_mode = "custom" really excludes n0's relays, and
[iroh] discovery = "none" switches the address lookup off — a Node ID is then
not enough to reach the endpoint, and a peer has to bring an address of its own.[iroh] discovery out keeps n0's address lookup, which is what a config
written before the key existed does.10.0.1.0/24 — a great many home
routers use it, and the tunnel swallowed the router's own addresses there. It picks the first
block the host will take.*.example.com means, and a
name the client decided to tunnel could be a name the server had no route for. There is one
rule now, on both sides.https:// backends and HTTP/2, [retry] and
[circuit_breaker] — the hop can be encrypted, tried again, and dropped when
it stays down.One line each; the full entry is in CHANGELOG.md.
client add --device --public-key, so a device that runs unattended needs no code.allow_ports and allow_ports_strict hold a client to the
ports it may reach and not only to the names.[limits], [backends] ca_file, [retry],
[circuit_breaker], [iroh] discovery and the six
[timeouts] phase keys — all optional, and each one leaves the old behaviour
in place when it is left out.https:// backends and HTTP/2, so the hop to a backend can be
encrypted.config.toml into a file of its own
beside it, so the configuration is not rewritten for the counters.localStorage into an encrypted store whose
master key the OS keychain holds.sudo chown -R 1000:1000 config secrets logs before the first start, and
with sudo — the files the old root image wrote are root-owned. Skipping it
does not always fail loudly: an iroh.key that cannot be opened is
regenerated, which is a new Node ID, and every client that stored the old one stops
finding the server.[peers] and [auth] refused unknown keys and the rest dropped them
silently; every section refuses them now, the top level included. Run the server once against
your config and fix what it names — a stray key at the top of the file is refused the
same way:
unknown field `stray_key`, expected one of `default_backend`, `debug`, `routes`, `server`, `iroh`, ... This build does not know that key: remove it, or fix its spelling.One key is worth naming in advance, because a config can carry it today:
[admin] token is not a key. The admin token is generated into
<config>.admin-token and that file is the only source of it, so a config that
names a token is refused rather than parsed and dropped — the operator finds out that the
token they set is not the token in use, instead of finding out when the management surface
answers 401.
config.toml puts its state in config.state.toml. The old
state.toml beside it is still read when the new one is not there yet, and the
counters are still read from config.toml, so existing lockouts survive. The
configuration directory has to stay writable.[server] tls_enabled,
tls_listen_addr, cert_path and key_path, the route-level
cert_path, key_path and redirect_to_https, and the
[acme] section. TLS is terminated by the backend now.[auth] enabled still defaults to false for a config
with no [auth] section, and the config file's own permissions are still not
gated. The CLI is unchanged apart from client add --public-key.:1.0.0 rather than :latest if you want the version to
stay put.10.0.1.0/24
and is now the first block the host will take out of 198.18.0.0/24,
198.19.0.0/24, 100.100.0.0/24, 172.26.0.0/24,
10.200.0.0/24 and 10.0.0.0/24. The addresses are internal to the
tunnel; a rule of your own that hard-codes 10.0.1.x needs updating.localStorage. They go
into an encrypted file whose master key the OS keychain holds. The migration is automatic
— a payload written before it is read once and its secrets are written into the encrypted
store. Nothing to do by hand.