Introduction
Spora is a peer-to-peer VPN. One person shares their Internet connection, another connects to it through a link. Traffic comes out at the sharer’s home connection, from a real residential IP, which makes it very hard to tell apart from the sharer’s own browsing.
This book covers the command-line client, spora. It is the same
networking core that powers the Spora apps, driven from a terminal. You can
use it to:
- share your connection with someone you trust, and keep a local log of what your connection was used for;
- connect through a friend’s shared connection, as a full VPN client on Linux, macOS, or Windows;
- run pieces of your own infrastructure, such as a relay, when you do not want to depend on the built-in one;
- automate any of the above: the CLI has a machine-readable mode designed for scripts and tests.
If you just want the two-command version, start with the Quickstart. The how-to guides walk through real setups, and the reference section describes every flag, event, and file.
A note on trust: the person who shares carries the traffic of the person who connects. Share with people you know, and connect through people who know you. The sharing guide explains the connection log that protects the sharer’s side of that bargain.
Quickstart
Two machines, two commands: share on the machine whose connection you want
to use, use on the machine that should tunnel through it.
Install
Download the latest release for your system from the
releases page and put the
binary somewhere on your PATH:
- Linux:
spora-<version>-linux-<arch>.tar.gz, a single static binary namedspora. - macOS:
spora-<version>-macos-<arch>.tar.gz(aarch64for Apple silicon,x86_64for Intel), a single binary namedspora. It is not notarized yet, so if a browser downloaded the archive, macOS will refuse to run the binary until you clear the quarantine flag (xattr -d com.apple.quarantine spora); fetching withcurlavoids the flag entirely. - Windows:
spora-<version>-windows-<arch>.zip, containingspora.exepluswintun.dll.spora useloads the DLL from next to the executable, so keep the two files together.
Or build from source (a Rust toolchain is the only requirement):
git clone https://github.com/Spora-VPN/spora.git
cd spora
cargo build --release -p spora-cli
# the binary is target/release/spora-cli; rename or alias it to `spora`
For a source build on Windows, spora use also needs wintun.dll from
wintun.net next to the executable.
Share
On the sharing machine:
spora share
It prints a link:
Share this URL with the peer that wants to connect:
https://spora.to/s/2m5cRp…?r=167.71.66.250:443
Send that link to the person who should connect. Treat it like a key to your connection: anyone who has it can use it. The link stays the same across restarts, and a local connection log records what your connection was used for. Leave the process running; Ctrl+C stops sharing.
Connect
On the connecting machine, paste the link (quote it, the ? matters to the
shell). The client needs privileges to set up the tunnel: root on Linux and
macOS, an elevated console on Windows.
sudo spora use "https://spora.to/s/2m5cRp…?r=167.71.66.250:443"
You will see something like:
Tunnel up on spora0 (10.11.0.2/24, fd00:5350::2/64), MTU 1280 (follows the path).
Routing all traffic through the tunnel.
Resolver: 100.64.0.53 (set via resolvectl; restored on exit).
Press Ctrl+C to disconnect.
That is a full VPN: all traffic now leaves through the sharer’s connection.
Check it with curl ifconfig.me: you should see the sharer’s IP. Behind
the scenes the two machines also try to establish a direct path to each
other and switch over when it works: your traffic then stops sharing the
relay’s bandwidth and usually takes a shorter path.
Ctrl+C disconnects and puts routing and DNS back exactly as they were.
Where to go next
- Share your connection: identities, the connection log, and running a share permanently.
- Connect through a friend: what the client changes on your machine and how to shape it.
- Find out why a connection failed: every run keeps a diagnostic record you can read afterwards.
Share your connection
spora share turns your machine into the exit for someone you trust. No
port forwarding, no public IP, no configuration: the sharer registers with
a relay, and the client reaches it through that relay until a direct path
is punched.
spora share
Your share URL is a credential
The printed https://spora.to/s/… link is the whole secret: whoever has it
can route traffic through your connection. Send it over a channel you
trust, and to people you trust. If it leaks, run spora share --fresh to
generate a new identity (and a new URL); the old link stops working.
The URL stays stable across restarts because the identity behind it is
persisted at ~/.config/spora/identity.bin. Copy that file to move a share
to another machine, or keep several identities with --identity-file.
One share serves one client at a time. If a second client connects with the same URL, it replaces the first.
The connection log
You carry someone else’s traffic, so the CLI keeps a local, private record of what your connection was used for: which client session reached which destination IP and port, when, and how much data moved. If your IP is ever implicated in something you did not do, this is your answer to “what actually happened”.
spora log query --ip 203.0.113.5 --from 2026-08-01 --to 2026-08-28
spora log sessions
The log lives at ~/.local/state/spora/connlog/<routing-key>/, keeps 90
days by default (--conn-log-retention-days), and never leaves your
machine. spora log hold pins a time range against retention if you have
been asked to preserve records. --no-conn-log disables logging entirely,
which also means you will have no record of what clients did with your IP.
--conn-log-sessions-only records who was connected and when, without
per-destination detail.
Be aware of the limits: destinations are IPs, not names, and a session that stayed on the relay path never directly observes the client’s own address. The log states what kind of evidence each recorded address is.
Your client’s DNS
By default the client resolves names through you. Its queries arrive at a
resolver address inside the tunnel (100.64.0.53), and the share answers
them from the resolvers your own machine uses (/etc/resolv.conf; the
adapters’ DNS servers on Windows), forwarding each query verbatim to one
of them and the answer verbatim back. The client never learns those
addresses, and it still cannot reach anything else on your LAN. Should
your resolvers stop answering, the share falls back to public ones
(8.8.8.8, 1.1.1.1) until they recover. spora share prints which
resolvers it is forwarding to.
--dns-upstream <ip[:port]> (repeatable) forwards to these servers
instead, and only to them: no public fallback. --no-dns-forwarder
switches the forwarding off; the client then needs public resolvers of
its own (spora use --dns).
Known limitation: if your system resolves over encrypted DNS configured in
the OS itself (DNS over TLS or HTTPS on Android, macOS, iOS or Windows,
as opposed to a local stub such as systemd-resolved doing it on the
machine’s behalf), the share still sends the client’s queries in plain
UDP to the same servers. They resolve, but not over the encrypted
transport you chose. With --os-routing the forwarder is wired in through
a DNAT rule, so it is unavailable under --no-nat.
Running it permanently
The share is a single foreground process, so a systemd unit is all it takes:
[Unit]
Description=Spora share
After=network-online.target
[Service]
ExecStart=/usr/local/bin/spora share
Restart=on-failure
User=spora
[Install]
WantedBy=multi-user.target
systemctl stop sends SIGTERM, which the CLI handles cleanly.
The kernel exit
By default the sharer terminates client flows in a userland network stack and re-originates them from ordinary sockets. That needs no privileges and works everywhere. On Linux you can instead let the kernel route and NAT the client’s packets through a TUN device:
sudo spora share --os-routing
This performs better under load and behaves like a router rather than a
proxy. It changes the firewall (iptables) and sysctls while running and
undoes those changes on exit. See spora share --help for the
--tun-addr, --tun-mtu, and --no-nat knobs.
Connect through a friend
spora use <URL> is a full VPN client. With just the URL it will:
- bring up a tunnel interface (
spora0on Linux, autunon macOS, a wintun adapter on Windows); - connect to the sharer, first through the relay in the URL, then directly when a path can be punched;
- route your traffic into the tunnel, without ever touching your system’s own default route;
- send your DNS queries to the sharer, whose exit answers them from its own resolvers (or to public resolvers of your choosing);
- follow the network path’s real packet-size budget, so the interface MTU is what the path actually supports instead of a guess;
- put everything back the way it was when you disconnect.
It needs privileges for all of that: sudo on Linux and macOS, an elevated
console on Windows. Only one managed tunnel runs at a time.
sudo spora use "https://spora.to/s/…"
What it deliberately does not touch
Your default route stays untouched. On Linux the tunnel lives in its own routing table behind policy rules; on macOS and Windows it uses two half-width routes that take precedence by being more specific. Your LAN keeps working: destinations your machine has a direct route to (your printer, your NAS) are never pulled into the tunnel.
The tunnel’s own housekeeping traffic (the connection to the relay, the path discovery probes, the direct connection to the sharer) is pinned to your physical network, whatever addresses it goes to, so it does not loop into the tunnel. In the rare situations where it cannot be pinned (no detectable uplink, for example) spora warns loudly instead of failing silently.
DNS
While connected, name lookups go to the sharer. The tunnel carries them to
a fixed resolver address inside it (100.64.0.53), and the sharer’s exit
answers each one from whatever resolvers the sharer’s own machine uses,
so names resolve exactly the way the sharer’s network sees the world,
including the resolvers that network hands out, which are often the ones
that work best (or at all) from there. You never learn the sharer’s
resolver addresses, and the sharer still carries no traffic to its own
LAN. One honest exception: Windows may
still consult other adapters’ resolvers in parallel (its multi-homed name
resolution); the tunnel’s resolver is made the preferred one, but a
strict lockdown would need firewall-level filtering that spora does not
install.
On exit the previous resolver settings are restored. If the process is
killed outright, the next start repairs the common leftovers (a replaced
resolv.conf on Linux, the per-service settings on macOS).
Pick different resolvers with --dns (repeatable), or keep your own setup
with --no-dns. Any resolver other than the sharer’s must be a public
address: the sharer does not carry traffic to private ones. A sharer that
runs with --no-dns-forwarder does not answer the tunnel’s resolver
address at all; connect to one with --dns and public resolvers.
MTU
The two ends discover how large a packet the path between them can carry
and the client sets the tunnel interface to exactly that, typically 1414
bytes on an ordinary connection. When the tunnel later switches to a
direct path, the value is measured again and re-applied. Pin it manually
with --mtu if you must; below 1280 you also need --no-ipv6, because
IPv6 requires 1280.
IPv6
The tunnel carries IPv6 by default. If the sharer’s connection has no
IPv6, v6 connections are reset immediately at the sharer rather than
hanging; most applications (browsers in particular) then retry over v4 on
their own. --no-ipv6 keeps IPv6 out of the tunnel entirely; note that
on a v6-capable network your v6 traffic then goes around the tunnel, not
through it.
Disconnecting
Ctrl+C, SIGTERM, closing the console window on Windows: all of them
restore routes, resolver, and interface before the process exits. If the
process is killed outright, the interface disappears with it, and the next
spora use cleans up whatever else was left.
Split tunnels and other client setups
The default spora use routes everything. These are the other shapes it
supports.
Split tunnel: only some destinations
Route only chosen prefixes through the sharer and leave the rest of your traffic alone:
sudo spora use "$URL" --route 203.0.113.0/24 --route 198.51.100.0/24
Only those networks go through the tunnel. Your machine’s more specific
routes still win, so a --route that overlaps your LAN does not cut you
off from it.
DNS still switches to the tunnel resolvers by default, and the resolvers
themselves are routed through the tunnel so lookups to them stay off your
uplink (with the Windows caveat described in
Connect through a friend). If you want your normal resolver instead, add
--no-dns; names will then resolve the way your local network sees them,
which may differ from what the routed destinations expect.
No routes at all
sudo spora use "$URL" --no-routes
Brings the interface up with its address and MTU and stops there: no
routes, no resolver changes. Add routes yourself with ip route, route,
or your tooling of choice. This is the “I know what I am doing” mode for
custom policy routing.
Attach to an interface you manage
spora use "$URL" --tun-name mytun0
Attach to a TUN device that already exists (Linux only). The CLI only moves packets; the interface’s address, MTU, routes, resolver, and cleanup are entirely yours, and nothing else on the machine is touched. This is the mode test harnesses drive. Two cautions: create the device and address it before starting, and never route the relay’s own address into the interface, or the tunnel’s transport would chase its own tail.
Addressing inside the tunnel
The client defaults to 10.11.0.2/24 and fd00:5350::2/64 inside the
tunnel. If those collide with networks you actually use, pick others with
--tun-addr and --tun-addr6. They must stay in private (RFC 1918 or
CGNAT) and ULA space respectively; sharers refuse other client addresses.
Choosing STUN servers
Direct-path discovery asks STUN servers for your public address. The
built-in list works for most people; override it with --stun host:port
(repeatable, tried in order) if your network filters them or you prefer
your own.
Keeping the relay path
--no-direct-upgrade disables the direct path entirely: the session stays
on the relay. Useful when a punched path is undesirable, for example when
testing relay behavior itself.
Run your own relay
By default a share registers with the built-in public relay, so links work with zero setup. Running your own removes that dependency, and it is a small job: the relay is a deliberately dumb UDP forwarder. It carries encrypted packets between the two ends without being able to read them, holds no keys, and speaks no protocol of its own beyond a registration message.
The relay ships as its own tarball in the same GitHub release
(spora-relay-…, which contains the spora-relay binary and the
spora-issuer token tool) and builds from the same repository (where a
source build names the binary plain relay).
# on a machine with a public address
spora-relay --port 51820
# on the sharing machine
spora share --relay your.host:51820
The relay’s address is baked into the share URL, so clients need nothing
extra. Repeat --relay to register with several relays; clients try them
IPv6-first, then in listed order, which makes a second relay a cheap
fallback. A hostname with
both A and AAAA records counts as one relay per address family, and gives
you IPv6 connectivity for free.
Skipping the relay entirely
If the sharing machine itself has a public, reachable address, no relay is needed at all:
spora share --direct your.public.host:51820
Clients then dial the sharer directly. Combine with --relay to offer
both and let clients fall back.
Restricting who may use your relay
An open relay forwards for any sharer that registers. To limit yours to sharers you have authorized, the relay supports capability tokens; the repository’s relay/README.md covers issuing them, the alternative TCP and Noise carriers for networks that block UDP or fingerprint QUIC, and the relay’s own session log.
Drive the CLI from scripts
Everything the CLI reports has a machine-readable form. This is the same surface Spora’s own test infrastructure drives, so it is kept stable and exact.
JSON events
With --json, share and use emit one JSON object per line on stdout
instead of prose:
spora share --json | while read -r line; do
event=$(jq -r .event <<<"$line")
...
done
The first events you care about are share_ready (carries the URL) and
tunnel_ready (the tunnel interface is up). path_activated tells you
which transport is actually carrying traffic, and fires again when the
session upgrades to a punched direct path. The complete vocabulary, with
every field, is in the JSON events reference.
Events are additive: new event types and new fields may appear over time, so parse by name and ignore what you do not know.
A tunnel your harness controls
For tests you usually do not want the CLI touching routing at all. Create the TUN yourself, then attach:
ip tuntap add dev t0 mode tun user "$USER"
ip addr add 10.231.0.2/24 dev t0
ip link set t0 mtu 1280 up
ip route add 198.51.100.7/32 dev t0 # route the systems under test, never the relay
spora use "$URL" --tun-name t0 --json
In this mode the CLI guarantees it changes nothing on the host beyond
pumping packets; tunnel_ready reports "mode": "attached". The MTU the
path discovers is still reported (path_mtu) so your harness can apply it
if it wants to.
Clean shutdown
SIGTERM and SIGINT both disconnect cleanly and restore any host changes;
processes exit zero. SIGKILL skips cleanup by definition; the next
spora use repairs the client-side leftovers it finds. On Windows,
closing the console counts as a clean shutdown.
Records for assertions
Every run also writes a diagnostic record: a structured account of what
was attempted and how it ended, with stable reason codes rather than
message strings. spora record show --json prints it, --record-id lets
you tag a run with your own correlation id, and --record-dir puts the
records where your harness expects them. See
Find out why a connection failed.
Find out why a connection failed
You do not have to reproduce a failure with more logging: every run of
share and use keeps a diagnostic record of what it attempted, what
failed, and why. When someone says “it did not work”, the record is the
thing to look at, and to send.
spora record list
when id role outcome took first failure
2026-08-28 18:53:04Z 6a9ec9d2 client never_connected 8.0s relay_dial connect_timeout
2026-08-28 14:02:11Z 0c81d7a2 client closed_locally 541.2s punch punch_timeout
spora record show walks the newest record step by step; show <id>
picks one. Failures carry a reason code from a fixed vocabulary, so two
people looking at two records can compare them meaningfully.
spora record show 6a9ec9d2
spora record export -o run.json # attach this when asking for help
Reading the common outcomes
never_connected, with a failed relay_dial step. The client never
reached the sharer. The reason code says how it died: connect_timeout
means nothing answered at all (the share is not running, the relay in the
URL is unreachable, or the network drops UDP); handshake_timeout means
something answered but the session never completed. If the sharer
advertises a TCP carrier, the client falls back to it automatically;
otherwise ask the sharer to add one.
Connected, but the punch steps failed. The session worked but stayed
on the relay: the record ends closed_locally (or closed_by_peer), the
punch steps show punch_timeout or similar, and there is no
direct path after line in spora record show. That is the designed
fallback, not an error: some NAT combinations (notably symmetric NAT on
both ends) cannot be punched. Expect the relay’s bandwidth, not
direct-path bandwidth.
Outcome lost, or repeated reconnecting. The path keeps dying. Look
at the quality samples (spora record show --samples): probe loss and
round trip time while the tunnel was up are recorded, and tell you whether
the problem is the path or the endpoints.
spora use refuses to start. The error names the cause directly:
missing privileges (run with sudo, or elevate the console), another
running instance (only one managed tunnel at a time), or on Windows a
missing wintun.dll.
When you report a problem
Send three things: the record (spora record export), the exact command
line, and spora build-info --json from both ends. The build info names
the exact source the binary was built from, so nobody debugs the wrong
version.
Command reference
The complete surface of the spora command line, rendered from the same
definitions the binary is built from. spora <command> --help prints the
same text.
spora share
Share over a tunnel. Loads (or creates on first run) a persistent identity at $XDG_CONFIG_HOME/spora/identity.bin so the share URL stays the same across invocations
spora share [OPTIONS]
--identity-file <IDENTITY_FILE>
Override the identity file path
--fresh (default false)
Generate a fresh identity for this run and overwrite the persisted one
--os-routing (default false)
Bypass the userland netstack: write client packets to a TUN device and let the kernel route/NAT them. Requires root (or CAP_NET_ADMIN). Linux only
--tun-addr <TUN_ADDR> (default 10.213.0.1/24)
TUN interface address in CIDR form (with –os-routing)
--tun-addr6 <TUN_ADDR6> (default fd00:5350::1/64)
TUN interface IPv6 address in CIDR form (with –os-routing). Must be a ULA (fc00::/7): clients may only source inner v6 traffic from ULA space, so a global address here would shadow a real prefix without ever being reachable
--tun-mtu <TUN_MTU> (default 1280)
TUN MTU (with –os-routing)
--no-nat (default false)
With –os-routing: don’t touch ip_forward or iptables. You are responsible for forwarding + NAT; per-client return routes on the TUN are still installed
--relay <RELAY> (repeatable)
Override the relay address(es) (host:port) used for registration and baked into the share URL. Repeat the flag for multiple relays (the sharer registers with all; the client tries them IPv6-first then in order). A hostname with both A and AAAA records counts as two relays
--direct <DIRECT> (repeatable)
Advertise a relay-less DIRECT endpoint (host:port) clients can dial straight to this sharer: no relay, no relay bandwidth. The sharer binds this port, so it must be publicly reachable. Repeat for several advertised addresses (they must share one port). Combine with –relay for a mix, or use alone for pure relay-less sharing
--tcp-relay <TCP_RELAY> (repeatable)
Advertise a TCP/TLS relay endpoint (host:port): a TCP relay carrying end-to-end TLS, for networks that block UDP/QUIC. The sharer connects out and parks connections at it. Repeat for several; combine with –relay to offer both (the client tries them in preference order)
--nz-relay <NZ_RELAY> (repeatable)
Advertise a Noise UDP (nz) relay endpoint (host:port): the dumb UDP relay carrying an end-to-end Noise datagram session, a high-entropy, non-QUIC-shaped transport for networks that fingerprint or throttle QUIC. Point it at a relay on a non-443 UDP port. Repeat for several; combine with –relay to offer both (the client tries them in order)
--stun <STUN> (repeatable)
Override the ordered STUN servers (host:port) used for direct-upgrade endpoint discovery. Repeat for fallbacks
--no-direct-upgrade (default false)
Keep the session on its initial relay/carrier path instead of attempting a direct upgrade
--relay-token <RELAY_TOKEN>
Capability token authorizing this sharer to use the relay(s), when the relay requires one. Accepts a base64url token (as printed by spora-issuer issue) or a path to a file containing it. Not needed for open-mode relays
--no-conn-log (default false)
Disable the connection log. By default the sharer keeps a local per-flow record (who connected to which destination, when) at $XDG_STATE_HOME/spora/connlog/<routing-key>/, the sharer’s own egress accountability record for answering abuse reports
--dns-upstream <DNS_UPSTREAM> (repeatable)
Answer clients’ DNS queries from these servers (ip or ip:port, repeatable, in preference order) instead of this host’s own resolvers. Used exclusively: no public fallback
--no-dns-forwarder (default false)
Don’t answer clients’ DNS queries at all: queries to the tunnel’s resolver address (100.64.0.53) are dropped like any other private destination, so clients must bring public resolvers of their own
--conn-log-dir <CONN_LOG_DIR>
Override the connection-log directory
--conn-log-retention-days <CONN_LOG_RETENTION_DAYS> (default 90)
Connection-log retention in days; older records are deleted (unless pinned by spora log hold)
--conn-log-sessions-only (default false)
Log sessions only (who was connected and when), without per-flow destination records
--no-record (default false)
Don’t keep a diagnostic record of how each connection went
--record-dir <RECORD_DIR>
Override the diagnostic-record directory (default: $XDG_STATE_HOME/spora/records/)
--record-label <RECORD_LABEL>
Label this machine in every record it writes
--record-id <RECORD_ID>
Tie the records from this run to something outside it: a ticket, a test run
--json (default false)
Emit newline-delimited JSON lifecycle events on stdout
spora use
Connect through a share URL. By default this is a full VPN client: it brings up the tunnel interface, routes traffic through it, sets the resolver, follows the path’s MTU, and restores everything on exit. Needs root (Administrator on Windows), except with –tun-name
spora use [OPTIONS] <URL>
<URL> (required)
The share URL (https://spora.to/s/…) received from the sharer
--stun <STUN> (repeatable)
Override the ordered STUN servers (host:port) used for direct-upgrade endpoint discovery. Repeat for fallbacks
--no-direct-upgrade (default false)
Keep the session on its initial relay/carrier path instead of attempting a direct upgrade
--tun-name <TUN_NAME>
Attach to this pre-created TUN instead of bringing up the tunnel interface yourself. The caller owns its address, MTU, routes and cleanup; nothing else on the host is touched (Linux only)
--tun-addr <TUN_ADDR> (default 10.11.0.2/24)
Address of the tunnel interface, in CIDR form. Must be private (RFC1918/CGNAT): sharers refuse other client sources
--tun-addr6 <TUN_ADDR6> (default fd00:5350::2/64)
IPv6 address of the tunnel interface, in CIDR form (must be a ULA, fc00::/7)
--no-ipv6 (default false)
Carry no IPv6 in the tunnel: no v6 address, no v6 routes. On a v6-capable host, v6 traffic then bypasses the tunnel
--route <ROUTE> (repeatable)
Route only this prefix into the tunnel (repeatable) instead of everything. The host’s more specific routes (its LANs) still win
--no-routes (default false)
Bring the interface up with its address and MTU but install no routes and leave the resolver alone: you route into it yourself
--dns <DNS> (repeatable)
Resolver to use while connected (repeatable). The default, 100.64.0.53, is the sharer’s DNS forwarder, which answers from the sharer’s own resolvers. Anything else is reached through the tunnel, so it must be public
--no-dns (default false)
Leave the host’s resolver configuration alone
--mtu <MTU>
Pin the interface MTU (576..=1500) instead of following the path’s discovered budget
--no-record (default false)
Don’t keep a diagnostic record of how the connection went
--record-dir <RECORD_DIR>
Override the diagnostic-record directory (default: $XDG_STATE_HOME/spora/records/)
--record-label <RECORD_LABEL>
Label this machine in every record it writes
--record-id <RECORD_ID>
Tie this run’s record to something outside it: a ticket, a test run
--json (default false)
Emit newline-delimited JSON lifecycle events on stdout
spora build-info
Print the source/build identity embedded in this executable
spora build-info [OPTIONS]
--json (default false)
Emit the complete build identity as JSON
spora log
Inspect the share’s connection log (see docs/connection-logging.md)
spora log <COMMAND>
spora log query
Query flows: “who connected to destination IP X during [from, to]”. Prints matching flows, the sessions they belong to (with everything known about the client’s outer address), and any log gaps or clock jumps overlapping the window
spora log query [OPTIONS]
--ip <IP>
Destination IP to match
--port <PORT>
Destination port to match
--from <FROM>
Window start: RFC3339 (2026-06-12T10:00:00Z), a date (2026-06-12), or unix seconds/milliseconds
--to <TO>
Window end (same formats as –from)
--json (default false)
Machine-readable JSON output (for export/handover)
--dir <DIR>
Log directory (default: derived from the identity file; overrides –identity-file when both are given)
--identity-file <IDENTITY_FILE>
Identity file used to locate the default log directory
spora log sessions
List sessions with their address records
spora log sessions [OPTIONS]
--dir <DIR>
--identity-file <IDENTITY_FILE>
spora log hold
Manage legal holds: time ranges pinned against retention expiry (e.g. after receiving a preservation request)
spora log hold <COMMAND>
spora log hold add
Pin [from, to] against the retention sweep
spora log hold add [OPTIONS] --from <FROM> --to <TO>
--from <FROM> (required)
--to <TO> (required)
--note <NOTE>
Why this hold exists (e.g. the case/reference number)
--dir <DIR>
Log directory; overrides –identity-file when both are given
--identity-file <IDENTITY_FILE>
Identity file used to locate the default log directory
spora log hold list
List active holds
spora log hold list [OPTIONS]
--dir <DIR>
--identity-file <IDENTITY_FILE>
spora log hold remove
Remove a hold by id (records it pinned become subject to retention again on the next sweep)
spora log hold remove [OPTIONS] <ID>
<ID> (required)
--dir <DIR>
--identity-file <IDENTITY_FILE>
spora log hold help
Print this message or the help of the given subcommand(s)
spora log hold help [COMMAND]
spora log hold help add
Pin [from, to] against the retention sweep
spora log hold help add
spora log hold help list
List active holds
spora log hold help list
spora log hold help remove
Remove a hold by id (records it pinned become subject to retention again on the next sweep)
spora log hold help remove
spora log hold help help
Print this message or the help of the given subcommand(s)
spora log hold help help
spora log help
Print this message or the help of the given subcommand(s)
spora log help [COMMAND]
spora log help query
Query flows: “who connected to destination IP X during [from, to]”. Prints matching flows, the sessions they belong to (with everything known about the client’s outer address), and any log gaps or clock jumps overlapping the window
spora log help query
spora log help sessions
List sessions with their address records
spora log help sessions
spora log help hold
Manage legal holds: time ranges pinned against retention expiry (e.g. after receiving a preservation request)
spora log help hold [COMMAND]
spora log help hold add
Pin [from, to] against the retention sweep
spora log help hold add
spora log help hold list
List active holds
spora log help hold list
spora log help hold remove
Remove a hold by id (records it pinned become subject to retention again on the next sweep)
spora log help hold remove
spora log help help
Print this message or the help of the given subcommand(s)
spora log help help
spora record
Read the diagnostic records of past connections: what was attempted, what failed, and why (see docs/diagnostic-record.md)
spora record <COMMAND>
spora record list
One line per record: when, which end, how it ended, and the first thing that failed
spora record list [OPTIONS]
--dir <DIR>
Record directory (default: $XDG_STATE_HOME/spora/records/)
--count <COUNT> (default 20)
How many records to list, newest first
--json (default false)
Machine-readable JSON output
spora record show
The full story of one record: every step, in order, with its verdict
spora record show [OPTIONS] [ID]
<ID>
Record id (or its first characters). Defaults to the newest
--dir <DIR>
--json (default false)
Machine-readable JSON output: the whole record, folded
--samples (default false)
Include the quality samples taken while the tunnel was up
spora record export
Write records out as JSON, for handing to someone else
spora record export [OPTIONS]
--dir <DIR>
--count <COUNT> (default 20)
How many records to export, newest first
--out <OUT>
Write to this file instead of standard output
spora record help
Print this message or the help of the given subcommand(s)
spora record help [COMMAND]
spora record help list
One line per record: when, which end, how it ended, and the first thing that failed
spora record help list
spora record help show
The full story of one record: every step, in order, with its verdict
spora record help show
spora record help export
Write records out as JSON, for handing to someone else
spora record help export
spora record help help
Print this message or the help of the given subcommand(s)
spora record help help
spora help
Print this message or the help of the given subcommand(s)
spora help [COMMAND]
spora help share
Share over a tunnel. Loads (or creates on first run) a persistent identity at $XDG_CONFIG_HOME/spora/identity.bin so the share URL stays the same across invocations
spora help share
spora help use
Connect through a share URL. By default this is a full VPN client: it brings up the tunnel interface, routes traffic through it, sets the resolver, follows the path’s MTU, and restores everything on exit. Needs root (Administrator on Windows), except with –tun-name
spora help use
spora help build-info
Print the source/build identity embedded in this executable
spora help build-info
spora help log
Inspect the share’s connection log (see docs/connection-logging.md)
spora help log [COMMAND]
spora help log query
Query flows: “who connected to destination IP X during [from, to]”. Prints matching flows, the sessions they belong to (with everything known about the client’s outer address), and any log gaps or clock jumps overlapping the window
spora help log query
spora help log sessions
List sessions with their address records
spora help log sessions
spora help log hold
Manage legal holds: time ranges pinned against retention expiry (e.g. after receiving a preservation request)
spora help log hold [COMMAND]
spora help log hold add
Pin [from, to] against the retention sweep
spora help log hold add
spora help log hold list
List active holds
spora help log hold list
spora help log hold remove
Remove a hold by id (records it pinned become subject to retention again on the next sweep)
spora help log hold remove
spora help record
Read the diagnostic records of past connections: what was attempted, what failed, and why (see docs/diagnostic-record.md)
spora help record [COMMAND]
spora help record list
One line per record: when, which end, how it ended, and the first thing that failed
spora help record list
spora help record show
The full story of one record: every step, in order, with its verdict
spora help record show
spora help record export
Write records out as JSON, for handing to someone else
spora help record export
spora help help
Print this message or the help of the given subcommand(s)
spora help help
JSON events
With --json, share and use print one JSON object per line on stdout.
Every object has an "event" name and a "v": 1 schema version. The
vocabulary is additive: new events and new fields may appear, so match on
names and ignore unknown members. Log text goes to stderr and never mixes
into the stream.
Lifecycle
| event | emitted by | meaning |
|---|---|---|
share_ready | share | The share is up and registered. Fields: url, routing_key, identity_file, tun_name (with --os-routing, else null), record_dir, upgrade_enabled. |
tunnel_ready | use | The tunnel interface is up and pumping. Fields: mode ("vpn" or "attached"), tun_name, record_dir, upgrade_enabled; in vpn mode also tun_addr, tun_addr6 (null with --no-ipv6), mtu, mtu_policy ("auto" or "fixed"), routes, dns, dns_method (null when the resolver was left alone). |
stopping | both | Shutdown has begun (Ctrl+C, SIGTERM, or console close). |
session_ended | share | The client session ended. Field: reason. |
Paths and transports
| event | emitted by | meaning |
|---|---|---|
relay_session_established | both | The relay-path session is up. Field: peer. |
path_activated | both | A transport can now carry tunnel traffic. Fields: carrier (quic, tcp_tls, nz), path (relay, direct_advertised, direct_punched), local, peer. Fires for the initial path and again after an acknowledged switch to a punched path, so automation never infers the active transport from log prose. |
direct_upgrade_succeeded | both | A direct path replaced the relay path. Fields: local, peer. |
direct_upgrade_failed | both | One direct-path attempt failed; the session stays on its current path and retries. Fields: code (a stable reason code), reason (human text). |
reconnecting | use | The transport died; the client is redialing. |
reconnected | use | The redial succeeded. |
MTU
| event | emitted by | meaning |
|---|---|---|
path_mtu | use | The path’s measured packet budget, reported after path discovery settles and again after a direct upgrade. Field: mtu. |
tun_mtu | use | The tunnel interface MTU was changed to match (VPN mode with the default --mtu policy). Field: mtu. |
Housekeeping
| event | emitted by | meaning |
|---|---|---|
conn_log_degraded | share | The connection log hit trouble (disk, overload) and marked a gap. Field: detail. |
The share URL
https://spora.to/s/<token>?r=<host>:<port>[&r=<host>:<port>…]
The URL is self-contained: everything a client needs is inside it, and possession of it is the only credential. Treat it accordingly.
<token>is 48 characters of base64url: a 20-byte routing key (identifies the share; also the QUIC connection id clients dial with) followed by a 16-byte secret (proves to the sharer that the client was given the URL). Rotate both withspora share --fresh.- Each
?r=names a relay endpoint, in the order clients should try them. IPv6 literals are bracketed:?r=[2001:db8::1]:443. Endpoints advertised with--direct,--tcp-relay, or--nz-relayare encoded here too, marked with their protocol. - The
https://spora.to/s/prefix makes the link clickable and lets the apps claim it. The CLI never contacts the website; if the link is opened in a browser without the app installed, the site shows an install page and does not log the token path.
Clients resolve relay hostnames at every connection attempt, prefer IPv6 addresses, and fall back through the list until one works, so a multi-relay URL keeps working when a relay dies.
Files and directories
The CLI follows the XDG conventions on Linux and macOS and the profile
directories on Windows. Overriding environment variables (XDG_CONFIG_HOME,
XDG_STATE_HOME) are honored.
| what | Linux and macOS | Windows |
|---|---|---|
| Identity (key material behind the share URL) | ~/.config/spora/identity.bin | %APPDATA%\spora\identity.bin |
| Connection log (per share identity) | ~/.local/state/spora/connlog/<routing-key>/ | %LOCALAPPDATA%\spora\connlog\<routing-key>\ |
| Diagnostic records | ~/.local/state/spora/records/ | %LOCALAPPDATA%\spora\records\ |
Notes:
- The identity file is the secret behind a stable share URL: mode
0600, written atomically. Copy it to move a share; delete it (or use--fresh) to revoke the URL.--identity-fileselects another path. - The connection log is SQLite; read it with
spora log, not by hand, and note that deleting the identity does not delete the log. It is your record of what your connection was used for, and it outlives the URL on purpose. - Records are plain JSON Lines, one file per run;
--record-dirmoves them and--no-recorddisables them. spora usein VPN mode also keeps small runtime files (an instance lock, resolver backups) under/run/spora/on Linux and/var/db/spora/on macOS; they are cleaned up automatically.- On Windows,
spora useexpectswintun.dllnext tospora.exe.