Skip to content

Commit 275262b

Browse files
committed
docs: readme, crate description, and cli help for a reader who does not know us
1 parent 44cf2cc commit 275262b

3 files changed

Lines changed: 90 additions & 88 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "pai-sho"
33
version = "0.5.1-dev"
44
edition = "2021"
5-
description = "Forward ports between your own machines, peer to peer over iroh. Neither side needs an account, a public IP, or an open inbound port. Only what you grant is reachable."
5+
description = "Forward ports between your own machines, peer to peer over iroh. Neither side needs an account, a public IP, or an open inbound port. A port is reachable only by the peers you grant it to."
66
license = "MIT"
77
repository = "https://github.com/cablehead/pai-sho"
88
keywords = ["iroh", "quic", "p2p", "port-forwarding", "nat-traversal"]

README.md

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<p align="center">
88
Forward ports between your own machines, peer to peer over <a href="https://github.com/n0-computer/iroh">iroh</a>.<br>
99
Neither side needs an account, a public IP, or an open inbound port.<br>
10-
Only what you grant is reachable.
10+
A port is reachable only by the peers you grant it to.
1111
</p>
1212

1313
<p align="center">
@@ -32,8 +32,11 @@ box invites your laptop and grants the port in the same command:
3232
```sh
3333
# build box
3434
pai-sho invite --expose 8080
35+
# 5hc4bjqfp6...7fd25613dd... one-time, valid 5 minutes
3536
```
3637

38+
Paste the invitation on your laptop, and give the box a name:
39+
3740
```sh
3841
# laptop
3942
pai-sho accept 5hc4bjqfp6...7fd25613dd... --as buildbox
@@ -42,9 +45,9 @@ curl http://buildbox.pai-sho:8080
4245

4346
### A laptop boots a VM
4447

45-
The roles reverse here: the consumer invites, and picks the name. You boot a
46-
dedicated VM per task, a [vibenv](https://github.com/cablehead/vibenv.dag), with
47-
no inbound ports. Invite it from your laptop before it boots:
48+
You boot a dedicated VM per task, a [vibenv](https://github.com/cablehead/vibenv.dag),
49+
with no inbound ports. This time the laptop does the inviting, and names the VM
50+
before it exists:
4851

4952
```sh
5053
pai-sho invite --as vibenv-ndyg
@@ -60,16 +63,17 @@ both:
6063
pai-sho daemon --accept 5hc4bjqfp6...7fd25613dd... -e 3001,7331
6164
```
6265

63-
It is projected on acceptance, with no manual step: an address on your laptop's
64-
private network, ports bound there under the name you chose. Only your laptop can
65-
reach it, and anyone else who dials is refused.
66+
As soon as it connects, the VM gets an address on your laptop's private network,
67+
and its two ports are bound there under the name you chose. Only your laptop is
68+
admitted; a dial from any other key is refused.
6669

6770
```sh
6871
curl http://vibenv-ndyg.pai-sho:3001
6972
open http://vibenv-ndyg.pai-sho:7331
7073
```
7174

72-
Close the laptop and reopen it. The connection restores and the ports rebind.
75+
If you close the laptop and reopen it, the connection comes back and the ports
76+
rebind.
7377

7478
[docs/scenarios.md](docs/scenarios.md) works both through in full.
7579

@@ -89,15 +93,15 @@ eget cablehead/pai-sho
8993

9094
Or grab a binary from [releases](https://github.com/cablehead/pai-sho/releases).
9195

92-
Homebrew also ships a supervised launch for the operator: a launchd service that
93-
creates the private network and points the system at the `.pai-sho` resolver. It
94-
does not start on its own; see [Setting up the network](#setting-up-the-network).
96+
Homebrew also ships a launchd service that creates the private network and
97+
points the system at the `.pai-sho` resolver. It does not start on its own; see
98+
[Setting up the network](#setting-up-the-network).
9599

96100
## Setting up the network
97101

98102
`--tun` puts each peer on a private `10.99.0.0/16` network. The daemon sits at
99103
`10.99.0.1`, peers land on `10.99.1.x`, and the daemon's resolver answers
100-
`*.pai-sho` in-stack on `10.99.0.53`.
104+
`*.pai-sho` at `10.99.0.53`.
101105

102106
### macOS
103107

@@ -110,12 +114,11 @@ sudo --preserve-env=XDG_CONFIG_HOME brew services start pai-sho
110114
```
111115

112116
`brew trust` records trust for your user. `sudo brew services` is the one root
113-
use Homebrew allows (it loads a launchd service, runs no build scripts); `sudo
114-
brew trust` and `sudo brew install` are refused, and that refusal is correct.
115-
`--preserve-env=XDG_CONFIG_HOME` matters only if you set `XDG_CONFIG_HOME`: plain
116-
`sudo` strips it, so brew looks for your trust file under `$HOME/.homebrew`
117-
instead of your real config home and refuses the tap. Preserving it points brew
118-
back where `brew trust` wrote. (Harmless if you don't set `XDG_CONFIG_HOME`.)
117+
use Homebrew allows; it loads a launchd service and runs no build scripts.
118+
`sudo brew trust` and `sudo brew install` are refused.
119+
`--preserve-env=XDG_CONFIG_HOME` matters only if you set `XDG_CONFIG_HOME`:
120+
plain `sudo` strips it, so brew looks for your trust file under
121+
`$HOME/.homebrew` and refuses the tap.
119122

120123
The service creates the utun, points the system at the `.pai-sho` resolver, and
121124
hands you the control socket, so the CLI needs no sudo:
@@ -146,9 +149,9 @@ pai-sho daemon --tun ps0
146149
Then send `.pai-sho` to `10.99.0.53`, for example with a dnsmasq
147150
`server=/pai-sho/10.99.0.53` forward.
148151

149-
Without `--tun`, surfaces fall back to loopback addresses (`127.0.1.x`) and you
150-
serve the resolver with `--resolver <addr>`. You lose the private network but keep
151-
the names.
152+
Without `--tun`, peers are bound at loopback addresses (`127.0.1.x`) and you
153+
serve the resolver yourself with `--resolver <addr>`. Names still resolve, to
154+
those addresses.
152155

153156
## Usage
154157

@@ -181,27 +184,26 @@ pai-sho [--socket <path>] <command>
181184
| `-a, --accept` | | Take up an invitation, or a peer's key, on startup (repeatable) |
182185
| `-e, --expose` | | Expose port to the `--accept` peers (repeat or comma-separate) |
183186
| `--key` | `~/.local/state/pai-sho/key` | Secret key path (created if missing) |
184-
| `--tun` | | Put surfaces on a private TUN network (`utun` on macOS, a pre-created device like `ps0` on Linux); the resolver answers in-stack on `10.99.0.53:53` |
187+
| `--tun` | | Put surfaces on a private TUN network (`utun` on macOS, a pre-created device like `ps0` on Linux); the resolver answers at `10.99.0.53:53` |
185188
| `--resolver` | | Loopback mode, an alternative to `--tun`: serve the `*.pai-sho` resolver on this UDP address (e.g. `127.0.0.1:5353`) |
186189
| `--name` | | This node's own name. The resolver answers `<name>.pai-sho` with `--host`, so a service reached locally and from a peer has one origin, which is what CORS needs |
187190
| `--socket-owner` | | Username to own the control socket, chowned right after bind. Lets the CLI skip sudo when the daemon runs as root |
188191
| `--socket-mode` | | Octal mode for the control socket, e.g. `660` |
189192

190193
## How it works
191194

192-
**Identity.** Each daemon has a stable key, an iroh endpoint ID backed by a
193-
keypair at `--key`. Because it does not change, a launcher can bake one
194-
operator key into every workload it boots.
195+
**Identity.** Each daemon has a stable key: an iroh endpoint ID, backed by a
196+
keypair stored at `--key`. Because it never changes, whatever boots your VMs can
197+
hand each one your laptop's key ahead of time.
195198

196-
**Grants.** Access is default deny. A port becomes reachable only through a grant
197-
that names the peers allowed to reach it, and is served to them alone. iroh
198-
proves the connecting peer's key cryptographically, so a grant names a proven
199-
identity, not a shareable address. You cannot hand out reach by leaking a string
199+
**Grants.** A port is reachable only by the peers a grant names, and is served
200+
to them alone. A grant names a key, and iroh proves the connecting peer holds
201+
that key, so a peer cannot pass its access on to another machine
200202
([ADR 0001](docs/adr/0001-directed-grants.md)).
201203

202204
**Invitations.** A connection from an unknown key is refused unless it carries a
203-
code from `invite`. The code is spent on use, and the peer it admitted survives
204-
restarts, so a reboot does not orphan a workload. An invitation is
205+
code from `invite`. The code is spent on use, and the peer it admitted is
206+
remembered across restarts. An invitation is
205207
`<key>.<code>`: the key says who to dial, the code admits you. When you already
206208
know a peer's key, `invite <key>` authorizes it with no secret created at all
207209
([ADR 0006](docs/adr/0006-invitations.md),
@@ -220,18 +222,18 @@ the key comes from `pai-sho key` on the machine you are granting to.
220222

221223
**The network.** With `--tun`, the daemon runs its own TCP/IP stack on a private
222224
network interface. The daemon sits at `10.99.0.1`, peers get addresses on
223-
`10.99.1.x`, and the resolver answers in-stack on `10.99.0.53:53`. On Linux the
225+
`10.99.1.x`, and the resolver answers at `10.99.0.53:53`. On Linux the
224226
interface is created ahead of time and owned by the daemon's user, so the daemon
225227
needs no elevated capability. On macOS the daemon creates a utun itself, which
226228
needs root.
227229

228-
**Surfaces.** A peer's ports are addressed together at one address, under the
229-
name you gave it, or a short form of its key if nothing named it. A peer is
230-
projected automatically the first time it announces a granted port. Because each
231-
peer owns its address, two peers can serve the same port without colliding.
232-
`project` overrides the automatic choice (pin an address with `--ip`, rename with
233-
`--as`), `unproject` takes a surface down, and projections survive a restart
234-
([ADR 0004](docs/adr/0004-peer-surfaces.md)).
230+
**Surfaces.** A peer's ports all live at one local address, under the name you
231+
gave it, or the first eight characters of its key if you gave none. That address
232+
with its ports is the peer's surface. It comes up by itself the first time the
233+
peer announces a granted port. Each peer has its own address, so two peers can
234+
both serve `8080`. `project` overrides the defaults (pin an address with `--ip`,
235+
rename with `--as`), `unproject` takes a surface down, and projections survive a
236+
restart ([ADR 0004](docs/adr/0004-peer-surfaces.md)).
235237

236238
**Resolver.** The daemon answers `<name>.pai-sho` from the live surface table, so
237239
`vibenv-ndyg.pai-sho` reaches that peer's ports and stops resolving when the peer goes
@@ -243,13 +245,13 @@ dnsmasq `server=/pai-sho/10.99.0.53` forward on Linux
243245
**Reconnection.** If the connection drops, both sides retry with exponential
244246
backoff. Projected surfaces stay put and rebind when the link returns.
245247

246-
**Structure.** `src/core/` decides and does no IO: admission, grants, and tunnel
247-
authorization, unit tested without a network. The shell in `peer.rs` feeds it
248-
events and carries out the actions it returns
249-
([ADR 0007](docs/adr/0007-pure-core.md)).
248+
**Structure.** The decisions (who may connect, which grants exist, whether a
249+
tunnel is allowed) live in `src/core/`, which does no IO and is unit tested
250+
without a network. `peer.rs` feeds it events and carries out the actions it
251+
returns ([ADR 0007](docs/adr/0007-pure-core.md)).
250252

251-
The rules that hold whatever you type are in
252-
[docs/scenarios.md](docs/scenarios.md#invariants).
253+
The [invariants](docs/scenarios.md#invariants) in `docs/scenarios.md` are the
254+
five things that hold no matter which commands you run.
253255

254256
## See also
255257

@@ -261,12 +263,11 @@ something you're working on.
261263
[SSH tunnels](https://www.ssh.com/academy/ssh/tunneling) need inbound access on at
262264
least one side. pai-sho works when neither machine has open inbound ports.
263265

264-
[WireGuard](https://www.wireguard.com/) has no control plane and no relays. It
265-
only goes direct, so a peer entry in the
266+
[WireGuard](https://www.wireguard.com/) only goes direct: a peer entry in the
266267
[config file](https://www.wireguard.com/quickstart/) needs an `Endpoint` with a
267-
routable address. There is no hole punching and no fallback. If both machines are
268-
behind NAT, you are standing up a bounce host yourself. Tailscale adds that
269-
machinery around WireGuard; pai-sho gets it from iroh, over QUIC.
268+
routable address, and there is no hole punching or relay to fall back to. If
269+
both machines are behind NAT you need a bounce host. Tailscale adds that
270+
machinery around WireGuard; pai-sho gets it from iroh.
270271

271272
[dumbpipe](https://github.com/n0-computer/dumbpipe) is the direct inspiration.
272273
[pigeons](https://pigeons.computer), SSH over iroh from the same team, is where
@@ -285,8 +286,8 @@ direct path. When it can't, a relay carries the traffic:
285286
[DERP](https://tailscale.com/kb/1232/derp-servers) for Tailscale,
286287
[iroh's relays](https://www.iroh.computer/docs/concepts/relay) for pai-sho, run
287288
by [n0](https://n0.computer/). That whole layer comes from
288-
[iroh](https://github.com/n0-computer/iroh). What Tailscale has and pai-sho does
289-
not is a row above all that.
289+
[iroh](https://github.com/n0-computer/iroh). Tailscale has one more layer above
290+
it, the top row here:
290291

291292
```
292293
Tailscale
@@ -304,16 +305,15 @@ A Tailscale node registers with the
304305
decides membership and hands it a filtered list of the peers it may see. A
305306
pai-sho box dials your laptop by public key, resolved by
306307
[iroh's address lookup](https://www.iroh.computer/docs/concepts/discovery).
307-
Nothing in that path can add a peer to your set, and there is nothing to sign up
308-
for.
308+
Address lookup only answers where a key is reachable; it cannot add a peer to
309+
your set, and it needs no sign-up.
309310

310-
### Specific ports, not a whole machine
311+
### One port at a time
311312

312313
Tailscale gives a peer an IP, and everything listening on it is reachable unless
313-
an [ACL](https://tailscale.com/kb/1018/acls) says otherwise. Default allow, then
314-
narrow it. pai-sho grants one port at a time to one key, and a peer with no
315-
grants sees nothing. Day to day the two feel much the same, since you type a
316-
name and a port either way.
314+
an [ACL](https://tailscale.com/kb/1018/acls) says otherwise. pai-sho starts with
315+
nothing reachable, and you grant one port at a time to one key. Day to day the
316+
two feel much the same, since you type a name and a port either way.
317317

318318
### Less to install
319319

@@ -326,21 +326,21 @@ position, so this only holds on loopback.
326326

327327
### Tailscale's ops story is much nicer
328328

329-
One [policy file](https://tailscale.com/kb/1337/policy-syntax) for the whole
330-
tailnet, so who-can-reach-what is a thing you read in a single place. pai-sho's
331-
answer is "which command did you run on which machine." A web UI is the obvious
332-
next step.
329+
Tailscale has one [policy file](https://tailscale.com/kb/1337/policy-syntax) for
330+
the whole tailnet, so you can read who can reach what in one place. In pai-sho
331+
that information is spread across whichever `invite` and `expose` commands ran
332+
on which machine, and `list` shows one daemon's view of it; nothing yet shows
333+
the whole picture.
333334

334335
## More
335336

336337
[docs/scenarios.md](docs/scenarios.md) works two flows end to end: a shared build
337338
box reached from a laptop, and a laptop booting a vibenv. Each says what has to
338339
be true and what travels between the machines, and why the commands took the
339-
shape they did. Its [invariants](docs/scenarios.md#invariants) are the shortest
340-
statement of the model.
340+
shape they did.
341341

342-
The [ADRs](docs/adr) record the decisions and how they moved: directed grants,
343-
two passes at enrollment before invitations landed, peer surfaces, the owned
344-
resolver, and the pure core.
342+
The [ADRs](docs/adr) record the decisions and which ones superseded which:
343+
directed grants, two passes at enrollment before invitations landed, peer
344+
surfaces, the owned resolver, and the pure core.
345345

346346
Questions or ideas: come by the [Discord](https://discord.com/invite/YNbScHBHrh).

src/main.rs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ mod tunnel;
2121
about = "Forward ports between your own machines, peer to peer over iroh",
2222
long_about = "Forward ports between your own machines, peer to peer over iroh. \
2323
Neither side needs an account, a public IP, or an open inbound port.\n\n\
24-
Machines link by invitation: one side extends it, the other takes it up. Access \
25-
is default deny. You grant a port to a specific peer's key, and that peer alone \
26-
can reach it.\n\n\
27-
Each peer's ports answer under a name you picked, like vibenv-ndyg.pai-sho.",
24+
Two machines link by invitation: one runs `invite`, the other runs `accept`. \
25+
Nothing is reachable until you grant a port to a peer's key, and then only that \
26+
peer can reach it.\n\n\
27+
A peer's ports come up under the name you gave it, like vibenv-ndyg.pai-sho.",
2828
version
2929
)]
3030
struct Cli {
@@ -57,9 +57,9 @@ pub enum Command {
5757
/// 127.0.0.1:5353). Off when omitted.
5858
#[arg(long)]
5959
resolver: Option<SocketAddr>,
60-
/// Use the TUN owned-network backend on this pre-created device (e.g.
61-
/// `ps0`). Surfaces bind on the TUN via a userspace stack, and the
62-
/// `.pai-sho` resolver answers in-stack on 10.99.0.53:53. Loopback when omitted.
60+
/// Put peers on a private network over this pre-created TUN device (e.g.
61+
/// `ps0`). Their ports bind there via a userspace stack, and the
62+
/// `.pai-sho` resolver answers at 10.99.0.53:53. Loopback when omitted.
6363
#[arg(long)]
6464
tun: Option<String>,
6565
/// Username to own the control socket, chowned right after bind (before
@@ -69,16 +69,16 @@ pub enum Command {
6969
/// Octal mode for the control socket, e.g. `660` (chmod'd after bind).
7070
#[arg(long = "socket-mode")]
7171
socket_mode: Option<String>,
72-
/// This node's own name; the owned resolver answers `<name>.pai-sho`
73-
/// with 127.0.0.1, so local traffic uses the same origin peers do.
72+
/// This node's own name. The resolver answers `<name>.pai-sho` with
73+
/// 127.0.0.1, so a service has the same origin locally and from a peer.
7474
#[arg(long)]
7575
name: Option<String>,
7676
},
7777

78-
/// Extend an invitation. Without a key, prints a one-time invitation valid
79-
/// 5 minutes. With one, authorizes that key alone and creates no secret.
78+
/// Invite a peer. Without a key, prints a one-time invitation valid 5
79+
/// minutes. With a key, authorizes that key alone and creates no secret.
8080
Invite {
81-
/// Peer's key, for when nothing secret can safely travel to it.
81+
/// Peer's key, for when no secret can safely be sent to it.
8282
/// See docs/adr/0003-host-attested-enrollment.md
8383
key: Option<String>,
8484
/// What to call the peer that takes this up
@@ -104,21 +104,23 @@ pub enum Command {
104104
peer: String,
105105
},
106106

107-
/// Grant a local port to named peers. Nothing is reachable without one.
107+
/// Grant a local port to specific peers. Nothing is reachable without a grant.
108108
#[command(group = ArgGroup::new("grantees").required(true).args(["to", "all"]))]
109109
Expose {
110+
/// Local port to grant
110111
port: u16,
111112
/// Peer key(s) to grant the port to
112113
#[arg(long = "to")]
113114
to: Vec<String>,
114-
/// Grant to every peer known right now. Not a standing rule: a peer
115-
/// admitted later gets nothing.
115+
/// Grant to every peer known right now. A peer admitted later gets
116+
/// nothing.
116117
#[arg(long = "all")]
117118
all: bool,
118119
},
119120

120-
/// Revoke grants for a port. Bare, it revokes every grant for that port.
121+
/// Revoke grants for a port. Without --to, every grant for that port.
121122
Unexpose {
123+
/// Local port to revoke
122124
port: u16,
123125
/// Revoke only this peer's grant; defaults to every grant for the port
124126
#[arg(long = "to")]
@@ -131,16 +133,16 @@ pub enum Command {
131133
/// Print this daemon's key
132134
Key,
133135

134-
/// Override where a peer's ports are bound. Peers are projected
135-
/// automatically, so this is only needed to pin an address or rename one.
136+
/// Override where a peer's ports are bound. Peers get an address and a name
137+
/// automatically, so this is only needed to pin one or rename it.
136138
/// See docs/adr/0004-peer-surfaces.md.
137139
Project {
138140
/// Peer to project (a key or the name you gave it)
139141
peer: String,
140142
/// Local address to bind at; allocated from 127.0.1.0/24 if omitted
141143
#[arg(long)]
142144
ip: Option<IpAddr>,
143-
/// Rename this peer's surface (e.g. `broker`)
145+
/// Rename this peer (e.g. `broker`)
144146
#[arg(long = "as")]
145147
name: Option<String>,
146148
},

0 commit comments

Comments
 (0)