Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 named spora.
  • macOS: spora-<version>-macos-<arch>.tar.gz (aarch64 for Apple silicon, x86_64 for Intel), a single binary named spora. 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 with curl avoids the flag entirely.
  • Windows: spora-<version>-windows-<arch>.zip, containing spora.exe plus wintun.dll. spora use loads 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

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:

  1. bring up a tunnel interface (spora0 on Linux, a utun on macOS, a wintun adapter on Windows);
  2. connect to the sharer, first through the relay in the URL, then directly when a path can be punched;
  3. route your traffic into the tunnel, without ever touching your system’s own default route;
  4. send your DNS queries to the sharer, whose exit answers them from its own resolvers (or to public resolvers of your choosing);
  5. follow the network path’s real packet-size budget, so the interface MTU is what the path actually supports instead of a guess;
  6. 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

eventemitted bymeaning
share_readyshareThe share is up and registered. Fields: url, routing_key, identity_file, tun_name (with --os-routing, else null), record_dir, upgrade_enabled.
tunnel_readyuseThe 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).
stoppingbothShutdown has begun (Ctrl+C, SIGTERM, or console close).
session_endedshareThe client session ended. Field: reason.

Paths and transports

eventemitted bymeaning
relay_session_establishedbothThe relay-path session is up. Field: peer.
path_activatedbothA 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_succeededbothA direct path replaced the relay path. Fields: local, peer.
direct_upgrade_failedbothOne direct-path attempt failed; the session stays on its current path and retries. Fields: code (a stable reason code), reason (human text).
reconnectinguseThe transport died; the client is redialing.
reconnecteduseThe redial succeeded.

MTU

eventemitted bymeaning
path_mtuuseThe path’s measured packet budget, reported after path discovery settles and again after a direct upgrade. Field: mtu.
tun_mtuuseThe tunnel interface MTU was changed to match (VPN mode with the default --mtu policy). Field: mtu.

Housekeeping

eventemitted bymeaning
conn_log_degradedshareThe 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 with spora 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-relay are 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.

whatLinux and macOSWindows
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-file selects 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-dir moves them and --no-record disables them.
  • spora use in 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 use expects wintun.dll next to spora.exe.