Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0b48913
add netsim scenario for lab
Rekseto Jun 16, 2026
5533a1a
netsim: adjust install-astrald script; docs: add running guide;
Rekseto Jun 16, 2026
c6f29cf
docs: expand running-as-a-service.md doc;
Rekseto Jun 17, 2026
87deced
netsim: keep astrald insance running
Rekseto Jun 17, 2026
6a2ba50
netsim: add configure-astral-agent task; wire into lab.story
Jun 17, 2026
b98a48d
netsim: fix configure-astral-agent verify file count for symlinks
Jun 18, 2026
161b3b1
netsim: add bootstrap-user task
Jun 18, 2026
d58eae7
netsim: add link-swarm task
Jun 18, 2026
ae0cb7a
netsim: add share-object task
Jun 18, 2026
ca3184f
netsim: widen install-astrald health probe + diagnose on failure
Jun 18, 2026
bf31595
netsim: extract verify.sh logic into verify.py (link-swarm, share-obj…
Jun 19, 2026
7cac1ee
netsim: align scenarios with swarm-membership vocabulary
astral-intern0 Jun 21, 2026
afb1880
netsim: link-swarm — assert symmetric swarm roster (guards #348)
astral-intern0 Jun 22, 2026
52c71c3
netsim: share-object — store an object on a swarm sibling
astral-intern0 Jun 22, 2026
238dfda
netsim: add per-flow story files; refresh README + running-as-a-service
astral-intern0 Jun 22, 2026
e45dcf2
netsim: trim verbose top-of-file docstrings in verify.py scripts
astral-intern0 Jun 22, 2026
2084e96
netsim: minimize task READMEs to a general description each
astral-intern0 Jun 22, 2026
5566ca6
netsim: consolidate agent artifacts into $HOME/info.json
astral-intern0 Jun 22, 2026
81d5391
netsim: add import-user task (configure node from an existing mnemonic)
astral-intern0 Jun 22, 2026
8e19a83
netsim: rename User-setup tasks to <flow>-software-key
astral-intern0 Jun 22, 2026
88ec7b1
netsim: drop accidentally-committed __pycache__; ignore it
astral-intern0 Jun 22, 2026
db00af7
netsim: embed a real mnemonic in import-user-software-key prompt
astral-intern0 Jun 22, 2026
6336052
netsim: use the provided 18-word mnemonic in import-user-software-key…
astral-intern0 Jun 22, 2026
dcbe480
netsim: point configure-astral-agent at the migrated skills repo
astral-intern0 Jun 22, 2026
5bf4e49
netsim: rename link-swarm scenario to adopt-node
astral-intern0 Jun 22, 2026
9518d78
netsim: rename the single-User-node stage astrald-user -> astrald-sin…
astral-intern0 Jun 22, 2026
526d6e4
netsim: replace share-object with object-store + read-remote-object
astral-intern0 Jun 22, 2026
9e41b45
netsim: rename stages to the node-topology scheme
astral-intern0 Jun 22, 2026
53b412f
netsim: parametrize object-store with --target self|peer
astral-intern0 Jun 22, 2026
8a83901
netsim: object-store --target uses astral addresses; adopt-node regis…
astral-intern0 Jun 22, 2026
b17f21e
netsim: minimize task prompts to natural, human-style requests
astral-intern0 Jun 22, 2026
8b78dab
netsim: add expel-node task + story (two-nodes -> two-nodes-expel)
astral-intern0 Jun 22, 2026
d3d70ea
netsim: redesign read-remote-object as an agent-driven peer read
astral-intern0 Jun 22, 2026
1663417
netsim: prompts that append to info.json must keep existing entries
astral-intern0 Jun 22, 2026
6a9de85
netsim: per-task result files instead of one shared info.json
astral-intern0 Jun 22, 2026
0c7124b
netsim: ship fixed payload.txt for object-store; tighten task prompts
astral-intern0 Jun 23, 2026
a52cf37
netsim: enable-tor task, store-only object-store, siblings.json, skil…
astral-intern0 Jun 23, 2026
8de785f
netsim: restore leave-lan + link-over-tor tasks + tor-link.story (Tor…
astral-intern0 Jun 23, 2026
d0d78e2
netsim: disable apt-daily in the lab image; drop per-task apt quiescing
astral-intern0 Jun 25, 2026
0311371
netsim: expel-node verify asserts swarm membership, not link teardown
astral-intern0 Jun 25, 2026
d78c366
netsim: node-setup prompts require the active contract, not just the key
astral-intern0 Jun 25, 2026
2a2136b
netsim: minimize task READMEs to a single tight paragraph each
astral-intern0 Jun 25, 2026
4d7c165
netsim: trim link-over-tor prompt; verify owns the Tor confirmation
astral-intern0 Jun 25, 2026
50d4081
netsim: add tasks/_lib astral-py verify library (client+tunnel, CLI f…
astral-intern0 Jun 25, 2026
f3e9f23
netsim: migrate verifiers to the astral-py client (CLI fallback for a…
astral-intern0 Jun 25, 2026
7f5488e
netsim: readability pass on verifier Python (shlex-quote CLI args, dr…
astral-intern0 Jun 26, 2026
60a8409
netsim: vendor astral-py as a submodule under tasks/_lib (replaces ex…
astral-intern0 Jun 26, 2026
04d9df0
netsim: rename verify lib netsim_astral.py -> astralapi.py
astral-intern0 Jun 26, 2026
3832741
netsim: leave-lan withdraws the LAN address instead of nftables-dropp…
astral-intern0 Jun 26, 2026
7e128b2
netsim: install qemu-guest-agent in the lab image (pairs with netsim …
astral-intern0 Jul 1, 2026
963554c
netsim: add configure-nat-tor task (relocate Tor into the NAT'd node'…
astral-intern0 Jul 1, 2026
f7c06d4
netsim: add NAT scenario milestone tasks (enter-nat, add-reflector) +…
astral-intern0 Jul 1, 2026
2e2e64e
netsim: add-reflector verify — assert nat armed (public 198.51.100.x)…
astral-intern0 Jul 1, 2026
6b928c8
netsim NAT: run astral-query INSIDE the netns for NAT'd nodes (fixes …
astral-intern0 Jul 1, 2026
316674b
netsim: add punch-nat task + kcp verify + nat-punch story (full NAT s…
astral-intern0 Jul 1, 2026
ad7b50e
netsim NAT: fix the hole-punch — inbound DNAT + arm-after-Tor ordering
astral-intern0 Jul 1, 2026
59bf438
netsim: give each scenario its own directory with a plain-words README
astral-intern0 Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule ".ai/system"]
path = .ai/system
url = git@github.com:cryptopunkscc/astral-docs.git
[submodule "netsim/tasks/_lib/astral-py"]
path = netsim/tasks/_lib/astral-py
url = ssh://git@git.satforge.dev/satforge/astral-py.git
91 changes: 91 additions & 0 deletions docs/running-as-a-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Running astrald as a service

`astrald` is a long-running daemon. Run it under systemd on Linux.

## Build

```shell
CGO_ENABLED=0 go build -o /usr/local/bin/astrald ./cmd/astrald
CGO_ENABLED=0 go build -o /usr/local/bin/astral-query ./cmd/astral-query
```

Go >= 1.25.0 is required. astrald uses pure-Go SQLite, so `CGO_ENABLED=0` builds a
static binary. The `./` prefix is required; `go build .` at the repo root builds
an empty stub.

## Root directory

astrald stores config, identity, and data under a root directory derived from
`$HOME`. A systemd service has no `$HOME`. Pass `-root <dir>` to set the root
explicitly, or set `Environment=HOME=<dir>`. The first start generates the node
identity — a `secp256k1` key at `<root>/config/node_key` — with no interaction.

## Unit

`/etc/systemd/system/astrald.service`:

```ini
[Unit]
Description=astral daemon

[Service]
ExecStart=/usr/local/bin/astrald -root /var/lib/astrald
Environment=HOME=/root
Restart=on-failure
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target
```

`Type=simple` is the systemd default and is omitted. astrald traps `SIGINT`, not
`SIGTERM`; `KillSignal=SIGINT` makes `systemctl stop` shut it down gracefully.

```shell
systemctl enable --now astrald
```

This unit runs astrald as root — the simplest setup. To run it as your own user
instead, install it as a user service: place the unit at
`~/.config/systemd/user/astrald.service`, drop `Environment=HOME=` and the `-root`
flag (config and data then default to `~/.config/astrald` and
`~/.local/share/astrald`), and run `systemctl --user enable --now astrald`.
`loginctl enable-linger $USER` keeps it running without an active login session.

## Health check

```shell
astral-query localnode:.spec
```

The local API listens on `tcp:127.0.0.1:8625` with anonymous access. `.spec` is a
built-in, always-available op. Exit code 0 means the node is up.

## Ports

Default transports bind all interfaces.

| Port | Proto | Purpose |
|---|---|---|
| 1791 | TCP | node links |
| 1792 | UDP | KCP transport |
| 1791 | UDP | UTP transport |
| 8822 | UDP | `ether` LAN discovery |
| 8625 | TCP 127.0.0.1 | local apphost API |
| 8624 | TCP 0.0.0.0 | apphost HTTP API |

## Imaging and snapshots

Which step you take depends on the capture type:

- **Disk image (cold):** stop astrald first for a clean on-disk state; keep the
unit enabled so it autostarts on boot.
- **Live RAM snapshot (e.g. netsim):** leave astrald running so it resumes
already-running on restore.

```shell
systemctl enable astrald
systemctl stop astrald # disk image only — skip for a live RAM snapshot
```

The identity at `<root>/config/node_key` persists across either capture.
2 changes: 2 additions & 0 deletions netsim/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
155 changes: 155 additions & 0 deletions netsim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# netsim scenarios for astrald

Test scaffolding that drives `netsim` to build and run `astrald` on a simulated
LAN. It contains no astrald Go source and modifies none.

`netsim` boots Ubuntu 26.04 cloud-image VMs on `10.77.0.0/24` with per-VM NAT. A
*task* is a host-side script that configures the VMs. A *story* runs a list of
tasks in one simulation and saves a named *stage*. `lab.story` builds the
`astrald-lab` stage: two nodes running astrald, with a Qwen Code operator on
`node1`.

## Layout

```
netsim/
tasks/ # each task: run.sh (+ verify.sh / verify.py) + README.md
install-astrald/ # build + run astrald as a service on each node
configure-astral-agent/ # install the astral-agent skill into the qwen operator
bootstrap-user-software-key/ # make node1 a User node, new key -> one-node
import-user-software-key/ # make node1 a User node, existing mnemonic -> one-node
adopt-node/ # adopt node2 into swarm + register node aliases -> two-nodes
object-store/ # node1 stores an object (--target localnode|node2) -> two-nodes-data[-peer]
read-remote-object/ # node1's agent reads node2's object over astral (used by read-remote-peer)
expel-node/ # node1 (User) permanently bans node2 from the swarm -> two-nodes-expel
_lib/ # shared verify library (astralapi.py) + astral-py submodule
scenarios/ # one dir per scenario: <name>.story + README.md (plain-words)
lab/ # null -> astrald-lab (fixture)
bootstrap-user-software-key/ # astrald-lab -> one-node (fixture)
import-user-software-key/ # astrald-lab -> one-node (alt.)
adopt-node/ # one-node -> two-nodes (fixture)
object-store/ # two-nodes -> two-nodes-data (store on node1)
object-store-peer/ # two-nodes -> two-nodes-data-peer (store on node2)
read-remote-peer/ # two-nodes -> two-nodes-peer-read (store on node2, then read it)
expel-node/ # two-nodes -> two-nodes-expel
tor-link/ # two-nodes -> two-nodes-tor (re-link over Tor)
nat-punch/ # two-nodes -> two-nodes-nat (NAT hole-punch)
link.sh # register tasks with netsim (idempotent; re-run anytime)
README.md
```

## Registering tasks

`netsim` discovers tasks only under `~/.local/share/netsim/tasks/`. `link.sh`
symlinks every task under `tasks/` — each folder containing a `run.sh` — there.
It is idempotent; re-run it after adding a task. The symlinks leave netsim's
shipped builtins intact.

```sh
./netsim/link.sh
netsim tasks # confirm: install-astrald is listed as a user task
```

## Verifier library

The `verify.py` oracles share `tasks/_lib/astralapi.py`, which reaches each
VM's apphost through the **astral-py** client vendored as a submodule at
`tasks/_lib/astral-py`. Initialize it once per worktree (`workon.sh` does not
`--recurse`); a missing submodule fails with a loud `ImportError`:

```sh
git submodule update --init netsim/tasks/_lib/astral-py
```

The verifiers fall back to the Go `astral-query` CLI for any op the client can't
serve, but the submodule must be present for `verify.py` to import.

## Lab

`lab.story` builds the full lab in one simulation: two nodes running astrald and
a Qwen Code operator on `node1`, equipped with the `astral-agent` skill.

```
# lab.story — the astrald lab, built in one netsim simulation.
# Result: a single stage with two nodes running astrald and a Qwen Code
# operator on node1, equipped with the astral-agent skill.
add-vm --hostname node1
add-vm --hostname node2
install-astrald
install-qwen-code --vm node1 --create-user
configure-astral-agent --vm node1
```

A story is a plain-text file with one `task [args...]` per line, shell-style
quoting, and `#` for full-line or trailing comments. `netsim story` boots one
simulation, runs the listed tasks in order in the same VMs, and saves a single
stage at the end. It stops at the first failing task. Order is significant:

* `add-vm --hostname node1` and `add-vm --hostname node2` use the `add-vm`
builtin; they create the two plain Ubuntu VMs on the LAN.
* `install-astrald` is the [custom task](tasks/install-astrald/README.md); with no
`--vm` it installs astrald on every running VM, so on both nodes. It runs
`run.sh` then `verify.sh` and fails the story unless astrald builds, starts, and
answers `astral-query localnode:.spec` on every node. The service is left
enabled and running, so the stage snapshots a live node that resumes
already-running on restore.
* `install-qwen-code --vm node1 --create-user` uses the `install-qwen-code`
builtin; it installs the Qwen Code CLI on `node1` and points it at the
inference endpoint. The builtin installs for user `tester`, which does not
exist on a fresh cloud image, so `--create-user` is required. `node2` stays a
plain astrald peer.
* `configure-astral-agent --vm node1` is a [custom task](tasks/configure-astral-agent/README.md);
it installs the `astral-agent` skill into the Qwen Code operator so it can drive
astrald from the skill's knowledge. The host must have `SATFORGE_SKILLS_DEPLOY_KEY`
set (a deploy key for the private skills repo) — see its README.

Both VMs must exist and run before `install-astrald`, astrald must be present
before the Qwen Code operator is layered on `node1`, and the operator must exist
before its skill is configured.

Register the custom tasks once (see [Registering tasks](#registering-tasks)),
then build the lab:

```sh
./netsim/link.sh
export SATFORGE_SKILLS_DEPLOY_KEY=~/.ssh/satforge_skills_deploy # see tasks/configure-astral-agent
netsim story --stage null --save astrald-lab netsim/scenarios/lab/lab.story
```

The result is the stage `astrald-lab`: `node1` and `node2` running astrald, with a
Qwen Code operator on `node1` equipped with the `astral-agent` skill. Re-enter it
with `netsim shell --stage astrald-lab`.

## Swarm pipeline

Each post-lab flow is its own scenario under `scenarios/<name>/` — a `<name>.story`
plus a plain-words `README.md` — layered on the previous stage (its `start`/`save`
stages are in the story header and README). Intermediate stages stay reusable, so
you can replay one flow without rebuilding the chain:

```
astrald-lab ─[bootstrap-user-software-key]→ one-node ─[adopt-node]→ two-nodes ─[object-store]→ two-nodes-data
```

```sh
netsim story --stage astrald-lab --save one-node netsim/scenarios/bootstrap-user-software-key/bootstrap-user-software-key.story
netsim story --stage one-node --save two-nodes netsim/scenarios/adopt-node/adopt-node.story
netsim story --stage two-nodes --save two-nodes-data netsim/scenarios/object-store/object-store.story
netsim story --stage two-nodes --save two-nodes-peer-read netsim/scenarios/read-remote-peer/read-remote-peer.story
netsim story --stage two-nodes --save two-nodes-expel netsim/scenarios/expel-node/expel-node.story
```

`expel-node` is a separate branch off `two-nodes`: the User on node1 permanently
bans node2, so the swarm roster shrinks (node2 drops out of `user.swarm_status`,
lands in `user.list_expelled`, and the link is torn down). It produces its own
`two-nodes-expel` stage rather than feeding the data-object chain.

Each story drives the Qwen operator through its `astral-agent` skill, then runs an
independent `verify.sh`/`verify.py` check — so a story is a pass/fail integration
test for that flow.

## Scope

The lab stands up two astrald nodes, links them into one User Swarm, stores an
object on a node, and reads it from a peer across the swarm. Nodes discover each
other on the shared L2 LAN via UDP 8822 (`ether`/`nearby`).
24 changes: 24 additions & 0 deletions netsim/link.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# link.sh — register every task under tasks/ as a netsim user task.
# netsim only discovers tasks in ~/.local/share/netsim/tasks/, so symlink each
# task dir (each folder under tasks/ with a run.sh) there. Idempotent; re-run anytime.
set -eu

# CDPATH= is an intentional one-shot env prefix for cd, not an assignment
# shellcheck disable=SC1007
repo=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
dest="${NETSIM_HOME:-$HOME/.local/share/netsim}/tasks"
mkdir -p "$dest"

found=0
# a "task" = any folder under tasks/ that contains a run.sh
for rs in "$repo"/tasks/*/run.sh; do
[ -f "$rs" ] || continue
d=$(dirname "$rs")
ln -sfn "$d" "$dest/$(basename "$d")"
echo "linked $(basename "$d")"
found=$((found + 1))
done

[ "$found" -gt 0 ] || { echo "no tasks (folders with run.sh) found in $repo/tasks" >&2; exit 1; }
echo "done: $found task(s) registered — run 'netsim tasks' to confirm"
10 changes: 10 additions & 0 deletions netsim/scenarios/adopt-node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# adopt-node

Joins two nodes into the same user's network so they trust each other.

- **Kind:** fixture · **Family:** foundation
- **Chain:** `one-node` → `two-nodes`
- **Steps:** adopt-node
- **Run:** `netsim story --stage one-node --save two-nodes netsim/scenarios/adopt-node/adopt-node.story`

One node brings a second node into its personal network as a sibling, then verifies both share the same user contract and see each other as linked peers. This is the stable two-node baseline the multi-node scenarios start from.
4 changes: 4 additions & 0 deletions netsim/scenarios/adopt-node/adopt-node.story
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# adopt-node.story — adopt node2 into node1's User swarm (symmetric roster).
# start: one-node save: two-nodes
# netsim story --stage one-node --save two-nodes netsim/scenarios/adopt-node/adopt-node.story
adopt-node
10 changes: 10 additions & 0 deletions netsim/scenarios/bootstrap-user-software-key/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# bootstrap-user-software-key

Turns a node into a user-controlled node by creating a fresh user identity.

- **Kind:** fixture · **Family:** identity
- **Chain:** `astrald-lab` → `one-node`
- **Steps:** bootstrap-user-software-key
- **Run:** `netsim story --stage astrald-lab --save one-node netsim/scenarios/bootstrap-user-software-key/bootstrap-user-software-key.story`

Creates a user account on the node and activates it with a contract, then confirms the node recognizes the user and accepts user commands. The result is a working user-controlled node that later scenarios build on.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# bootstrap-user-software-key.story — node1 becomes a User-controlled node.
# start: astrald-lab save: one-node
# netsim story --stage astrald-lab --save one-node netsim/scenarios/bootstrap-user-software-key/bootstrap-user-software-key.story
bootstrap-user-software-key
10 changes: 10 additions & 0 deletions netsim/scenarios/expel-node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# expel-node

Tests permanently banning another node from a shared swarm.

- **Kind:** scenario · **Family:** network
- **Chain:** `two-nodes` → `two-nodes-expel`
- **Steps:** expel-node
- **Run:** `netsim story --stage two-nodes --save two-nodes-expel netsim/scenarios/expel-node/expel-node.story`

One node expels another: it goes on a blocklist and is dropped from the active members, and the test confirms it is blocked and no longer on the roster. This is how a swarm enforces membership and keeps out unwanted nodes.
4 changes: 4 additions & 0 deletions netsim/scenarios/expel-node/expel-node.story
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# expel-node.story — node1 (the User) permanently bans node2 from its swarm.
# start: two-nodes save: two-nodes-expel
# netsim story --stage two-nodes --save two-nodes-expel netsim/scenarios/expel-node/expel-node.story
expel-node
10 changes: 10 additions & 0 deletions netsim/scenarios/import-user-software-key/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# import-user-software-key

Tests importing an existing user identity into a node from a recovery phrase.

- **Kind:** scenario · **Family:** identity
- **Chain:** `astrald-lab` → `one-node`
- **Steps:** import-user-software-key
- **Run:** `netsim story --stage astrald-lab --save one-node netsim/scenarios/import-user-software-key/import-user-software-key.story`

Takes an existing recovery phrase and uses it to rebuild the keys that make the node a user node with an active contract, then confirms the node identifies as that user. This is the alternative to bootstrap-user-software-key (both yield the one-node state) for the recover-an-identity path.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# import-user-software-key.story — make node1 a User node from an EXISTING mnemonic
# (embedded in the task's prompt.md; alternative to bootstrap-user-software-key).
# Optional env ASTRAL_USER_ID makes verify assert the derived id.
# start: astrald-lab save: one-node
# netsim story --stage astrald-lab --save one-node netsim/scenarios/import-user-software-key/import-user-software-key.story
import-user-software-key
10 changes: 10 additions & 0 deletions netsim/scenarios/lab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# lab

Builds the shared test lab: two astrald nodes plus an AI operator.

- **Kind:** fixture · **Family:** foundation
- **Chain:** `null` → `astrald-lab`
- **Steps:** add-vm · install-astrald · install-qwen-code · configure-astral-agent
- **Run:** `netsim story --stage null --save astrald-lab netsim/scenarios/lab/lab.story`

Creates two virtual machines and installs astrald on each so they can work together over a network. It also sets up Qwen Code, an AI assistant, on the first machine with a skill for talking to astrald. This baseline is the foundation every other scenario starts from.
9 changes: 9 additions & 0 deletions netsim/scenarios/lab/lab.story
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# lab.story — the astrald lab, built in one netsim simulation.
# start: null save: astrald-lab
# Result: a single stage with two nodes running astrald and a Qwen Code
# operator on node1, equipped with the astral-agent skill.
add-vm --hostname node1
add-vm --hostname node2
install-astrald
install-qwen-code --vm node1 --create-user
configure-astral-agent --vm node1
12 changes: 12 additions & 0 deletions netsim/scenarios/nat-punch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# nat-punch

Two NAT'd peers hole-punch to a direct connection, coordinating over Tor.

- **Kind:** scenario · **Family:** network
- **Chain:** `two-nodes` → `two-nodes-nat`
- **Steps:** add-vm · install-astrald · enable-tor · enter-nat · configure-nat-tor · add-reflector · punch-nat
- **Run:** `netsim story --stage two-nodes --save two-nodes-nat netsim/scenarios/nat-punch/nat-punch.story`

Both nodes are put behind their own NAT so they have no direct path to each other. A public reflector node tells each one its outside address, and they use a Tor link to coordinate a simultaneous connection attempt. On success the pair ends up talking over a direct kcp link instead of relaying through Tor.

> **Status:** works end to end (direct kcp link verified on both peers). The simulated NAT is currently a permissive *full-cone* NAT; a stricter, filtered NAT that a punch must genuinely defeat is under evaluation.
Loading