From 9966b5040771c433c078c2f659f064278629eb06 Mon Sep 17 00:00:00 2001 From: Aleksandr Bogomiakov Date: Sat, 3 Oct 2026 02:14:43 +0100 Subject: [PATCH] Rename the CLI package to tsunagi and its executable to tsng The command line package is now `tsunagi` and builds the `tsng` executable, which the packages, the unit, the installers and the workflows install and run. The core library package becomes `tsunagi-core`, keeping the library name `tsunagi`, so no code changes `use tsunagi::...`. Directories follow (crates/tsunagi-core, crates/tsunagi). Every command shown to a person is now `tsng ...`. Co-Authored-By: Claude --- .gitea/workflows/release.yml | 16 +- .github/workflows/release.yml | 16 +- AGENTS.md | 34 +-- Cargo.lock | 52 ++--- README.md | 106 +++++----- crates/tsunagi-cli/Cargo.toml | 47 ----- crates/tsunagi-cli/tests/identity.rs | 56 ----- crates/tsunagi-core/Cargo.toml | 113 ++++++++++ .../src/agent/events.rs | 0 .../src/agent/mod.rs | 2 +- .../src/agent/network.rs | 2 +- .../src/agent/session.rs | 0 .../src/agent/shutdown.rs | 0 .../src/agent/status.rs | 0 .../{tsunagi => tsunagi-core}/src/config.rs | 0 .../src/dataplane/mod.rs | 0 .../src/dataplane/relay.rs | 0 .../src/dataplane/relay_tests.rs | 0 .../src/dataplane/routing/envelope.rs | 0 .../src/dataplane/routing/flow.rs | 0 .../src/dataplane/routing/mod.rs | 0 .../src/dataplane/transport/fragments.rs | 0 .../src/dataplane/transport/iroh_link.rs | 0 .../src/dataplane/transport/mod.rs | 0 .../src/dataplane/tunnel.rs | 0 .../src/discovery.rs | 0 .../src/discovery/mainline.rs | 0 .../src/discovery/worker.rs | 0 .../{tsunagi => tsunagi-core}/src/dns/mod.rs | 0 .../src/dns/publish/macos.rs | 0 .../src/dns/publish/mock.rs | 0 .../src/dns/publish/mod.rs | 0 .../src/dns/publish/resolved.rs | 0 .../src/dns/publish/unsupported.rs | 0 .../src/dns/publish/windows.rs | 0 .../src/dns/server.rs | 0 .../{tsunagi => tsunagi-core}/src/dns/zone.rs | 0 crates/{tsunagi => tsunagi-core}/src/error.rs | 0 .../src/identity/mod.rs | 0 .../src/identity/network.rs | 0 .../{tsunagi => tsunagi-core}/src/ipc/mod.rs | 4 +- .../{tsunagi => tsunagi-core}/src/ipc/unix.rs | 0 .../src/ipc/windows.rs | 4 +- crates/{tsunagi => tsunagi-core}/src/lib.rs | 0 crates/{tsunagi => tsunagi-core}/src/net.rs | 0 .../src/overlay/broadcast.rs | 0 .../src/overlay/config.rs | 0 .../src/overlay/exit.rs | 0 .../src/overlay/hostrules/exit.rs | 0 .../src/overlay/hostrules/linux.rs | 0 .../src/overlay/hostrules/linux_exit.rs | 0 .../src/overlay/hostrules/macos.rs | 0 .../src/overlay/hostrules/mod.rs | 0 .../src/overlay/hostrules/windows.rs | 2 +- .../src/overlay/interface.rs | 0 .../src/overlay/mod.rs | 0 .../src/overlay/packet.rs | 0 .../src/overlay/provision/factory.rs | 0 .../src/overlay/provision/linux.rs | 0 .../src/overlay/provision/macos.rs | 0 .../src/overlay/provision/mock.rs | 0 .../src/overlay/provision/mod.rs | 0 .../src/overlay/provision/privilege.rs | 10 +- .../src/overlay/provision/unsupported.rs | 0 .../src/overlay/provision/windows.rs | 0 .../src/overlay/router.rs | 0 .../src/overlay/tun.rs | 10 +- .../src/proto/frame.rs | 0 .../src/proto/handshake.rs | 0 .../src/proto/message.rs | 0 .../src/proto/mod.rs | 0 .../src/state/allocator.rs | 0 .../src/state/mod.rs | 0 .../src/storage/cache.rs | 0 .../src/storage/lock.rs | 0 .../src/storage/mod.rs | 0 .../src/storage/state.rs | 0 crates/{tsunagi => tsunagi-core}/src/task.rs | 0 .../{tsunagi => tsunagi-core}/src/testing.rs | 0 .../tests/authentication.rs | 0 .../tests/cache_and_state.rs | 0 .../tests/device_identity.rs | 0 .../tests/discovery.rs | 0 .../tests/discovery_lifecycle.rs | 0 .../tests/end_to_end.rs | 0 crates/tsunagi-core/tests/identity.rs | 173 +++++++++++++++ .../tests/leaving.rs | 0 .../tests/mainline.rs | 0 .../tests/mainline_socket.rs | 0 .../tests/multi_peer.rs | 0 .../tests/network_isolation.rs | 0 .../tests/resilience.rs | 0 .../tests/restart.rs | 0 crates/tsunagi-tcp-tls/Cargo.toml | 4 +- crates/tsunagi-tray/Cargo.toml | 2 +- crates/tsunagi-wg-quic/Cargo.toml | 4 +- crates/tsunagi-wg-quic/tests/local_control.rs | 2 +- crates/tsunagi-wireguard/Cargo.toml | 4 +- crates/tsunagi/Cargo.toml | 113 ++-------- crates/{tsunagi-cli => tsunagi}/src/main.rs | 155 +++++++------- .../tests/dns_service.rs | 6 +- crates/tsunagi/tests/identity.rs | 199 ++++-------------- .../tests/network_cli.rs | 8 +- dist/arch/README.md | 8 +- dist/arch/build-pkg.sh | 4 +- dist/deb/build-deb.sh | 6 +- dist/linux/README.md | 14 +- dist/linux/install.sh | 14 +- dist/linux/tsunagi.service | 2 +- dist/macos/README.md | 12 +- dist/macos/cy.hexor.tsunagi.plist | 4 +- dist/windows/README.md | 8 +- docs/architecture.md | 4 +- docs/broadcast.md | 12 +- docs/exit-node.md | 10 +- docs/mainline-dht.md | 4 +- docs/routing.md | 2 +- docs/sync-model.md | 4 +- docs/testing.md | 8 +- docs/wireguard.md | 6 +- 120 files changed, 631 insertions(+), 635 deletions(-) delete mode 100644 crates/tsunagi-cli/Cargo.toml delete mode 100644 crates/tsunagi-cli/tests/identity.rs create mode 100644 crates/tsunagi-core/Cargo.toml rename crates/{tsunagi => tsunagi-core}/src/agent/events.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/agent/mod.rs (99%) rename crates/{tsunagi => tsunagi-core}/src/agent/network.rs (99%) rename crates/{tsunagi => tsunagi-core}/src/agent/session.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/agent/shutdown.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/agent/status.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/config.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/relay.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/relay_tests.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/routing/envelope.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/routing/flow.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/routing/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/transport/fragments.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/transport/iroh_link.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/transport/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dataplane/tunnel.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/discovery.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/discovery/mainline.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/discovery/worker.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/publish/macos.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/publish/mock.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/publish/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/publish/resolved.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/publish/unsupported.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/publish/windows.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/server.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/dns/zone.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/error.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/identity/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/identity/network.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/ipc/mod.rs (99%) rename crates/{tsunagi => tsunagi-core}/src/ipc/unix.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/ipc/windows.rs (99%) rename crates/{tsunagi => tsunagi-core}/src/lib.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/net.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/broadcast.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/config.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/exit.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/hostrules/exit.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/hostrules/linux.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/hostrules/linux_exit.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/hostrules/macos.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/hostrules/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/hostrules/windows.rs (99%) rename crates/{tsunagi => tsunagi-core}/src/overlay/interface.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/packet.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/factory.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/linux.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/macos.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/mock.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/privilege.rs (96%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/unsupported.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/provision/windows.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/router.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/overlay/tun.rs (98%) rename crates/{tsunagi => tsunagi-core}/src/proto/frame.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/proto/handshake.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/proto/message.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/proto/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/state/allocator.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/state/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/storage/cache.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/storage/lock.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/storage/mod.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/storage/state.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/task.rs (100%) rename crates/{tsunagi => tsunagi-core}/src/testing.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/authentication.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/cache_and_state.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/device_identity.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/discovery.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/discovery_lifecycle.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/end_to_end.rs (100%) create mode 100644 crates/tsunagi-core/tests/identity.rs rename crates/{tsunagi => tsunagi-core}/tests/leaving.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/mainline.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/mainline_socket.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/multi_peer.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/network_isolation.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/resilience.rs (100%) rename crates/{tsunagi => tsunagi-core}/tests/restart.rs (100%) rename crates/{tsunagi-cli => tsunagi}/src/main.rs (97%) rename crates/{tsunagi-cli => tsunagi}/tests/dns_service.rs (99%) rename crates/{tsunagi-cli => tsunagi}/tests/network_cli.rs (99%) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 41b13e7..fcc2eba 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -98,9 +98,9 @@ jobs: # one lint to keep the musl build log clean. A real link error still # fails the build. export RUSTFLAGS="${RUSTFLAGS:-} -A linker_messages" - cargo zigbuild --release --locked --target "${{ matrix.target }}" --bin tsunagi + cargo zigbuild --release --locked --target "${{ matrix.target }}" --bin tsng else - cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi + cargo build --release --locked --target "${{ matrix.target }}" --bin tsng fi # The tray GUI. On Linux it is built against glibc (an old baseline for @@ -136,8 +136,8 @@ jobs: stage="dist/pkg/${{ matrix.asset }}" mkdir -p "$stage" - bin="tsunagi" - [[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe" + bin="tsng" + [[ "${{ runner.os }}" == "Windows" ]] && bin="tsng.exe" cp "target/${{ matrix.target }}/release/${bin}" "$stage/" # The tray GUI alongside it, where it was built (Linux: x86_64 only). @@ -186,7 +186,7 @@ jobs: set -euo pipefail version="${GITHUB_REF_NAME#v}" bash dist/deb/build-deb.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.deb_arch }}" \ "$version" \ "tsunagi_${version}_${{ matrix.deb_arch }}.deb" @@ -202,7 +202,7 @@ jobs: version="${GITHUB_REF_NAME#v}" pkgver="${version//-/_}" bash dist/arch/build-pkg.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.pkg_arch }}" \ "$version" \ "tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" @@ -218,11 +218,11 @@ jobs: pkgver="${version//-/_}" tray="target/${{ matrix.gnu_target }}/release/tsunagi-tray" bash dist/deb/build-deb.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.deb_arch }}" "$version" \ "tsunagi-gui_${version}_${{ matrix.deb_arch }}.deb" "$tray" bash dist/arch/build-pkg.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.pkg_arch }}" "$version" \ "tsunagi-gui-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "$tray" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06a9189..1694789 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,9 +100,9 @@ jobs: # one lint to keep the musl build log clean. A real link error still # fails the build. export RUSTFLAGS="${RUSTFLAGS:-} -A linker_messages" - cargo zigbuild --release --locked --target "${{ matrix.target }}" --bin tsunagi + cargo zigbuild --release --locked --target "${{ matrix.target }}" --bin tsng else - cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi + cargo build --release --locked --target "${{ matrix.target }}" --bin tsng fi # The tray GUI. On Linux it is built against glibc (an old baseline for @@ -138,8 +138,8 @@ jobs: stage="dist/pkg/${{ matrix.asset }}" mkdir -p "$stage" - bin="tsunagi" - [[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe" + bin="tsng" + [[ "${{ runner.os }}" == "Windows" ]] && bin="tsng.exe" cp "target/${{ matrix.target }}/release/${bin}" "$stage/" # The tray GUI alongside it, where it was built (Linux: x86_64 only). @@ -188,7 +188,7 @@ jobs: set -euo pipefail version="${GITHUB_REF_NAME#v}" bash dist/deb/build-deb.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.deb_arch }}" \ "$version" \ "tsunagi_${version}_${{ matrix.deb_arch }}.deb" @@ -204,7 +204,7 @@ jobs: version="${GITHUB_REF_NAME#v}" pkgver="${version//-/_}" bash dist/arch/build-pkg.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.pkg_arch }}" \ "$version" \ "tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" @@ -220,11 +220,11 @@ jobs: pkgver="${version//-/_}" tray="target/${{ matrix.gnu_target }}/release/tsunagi-tray" bash dist/deb/build-deb.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.deb_arch }}" "$version" \ "tsunagi-gui_${version}_${{ matrix.deb_arch }}.deb" "$tray" bash dist/arch/build-pkg.sh \ - "target/${{ matrix.target }}/release/tsunagi" \ + "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.pkg_arch }}" "$version" \ "tsunagi-gui-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "$tray" diff --git a/AGENTS.md b/AGENTS.md index 241f358..1795cfb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ scripts when improving the interactive workflow. Keep these separate. Crossing them is the main thing to review for. - **Control plane vs data plane.** The separation is **logical, not physical**. - The control protocol in `crates/tsunagi/src/proto/` knows nothing about + The control protocol in `crates/tsunagi-core/src/proto/` knows nothing about packets, and a protocol crate knows nothing about the control protocol; either can be replaced on its own. Both may ride on iroh — refusing to would throw away iroh's NAT traversal and force the data plane to @@ -70,9 +70,9 @@ Keep these separate. Crossing them is the main thing to review for. ownership checks or add broadcast flooding to encrypted transit. - **Plugins never learn reachability.** An `IpPlugin` is handed a `PacketLink` per peer and moves datagrams over it. Addresses, hole punching and relays - belong to `crates/tsunagi/src/dataplane/transport/`. A plugin announcement says *who*, never + belong to `crates/tsunagi-core/src/dataplane/transport/`. A plugin announcement says *who*, never *where*. -- **The core never parses a plugin payload.** See `crates/tsunagi/src/dataplane/mod.rs`. Only +- **The core never parses a plugin payload.** See `crates/tsunagi-core/src/dataplane/mod.rs`. Only `crates/tsunagi-wg-quic/src/announcement.rs` interprets `wg-quic` payloads, and only after bounding every field. A data plane failure must never stop the control plane. @@ -81,7 +81,7 @@ Keep these separate. Crossing them is the main thing to review for. inbound packets are dropped unless their source is the address derived for the peer that sent them. Never trust an address a peer announces. - **Signed state is the only durable agreement.** A fact that must survive a - participant being away goes in `crates/tsunagi/src/state/` as a record signed by its + participant being away goes in `crates/tsunagi-core/src/state/` as a record signed by its author, never in a session. Merging is deterministic, an older version never rolls back a newer one, and absence from a snapshot is not deletion. Never add a vote or a quorum: a majority is not a trust root here, and it would @@ -115,7 +115,7 @@ Keep these separate. Crossing them is the main thing to review for. normal path *and* the important failures. See [docs/testing.md](docs/testing.md). Do not add tests for getters or to move a coverage number. 2. **Never change `IDENTITY_SCHEME`, the derivation labels, or the transcript - encoding** in `crates/tsunagi/src/identity/network.rs` and `crates/tsunagi/src/proto/handshake.rs` without + encoding** in `crates/tsunagi-core/src/identity/network.rs` and `crates/tsunagi-core/src/proto/handshake.rs` without treating it as an incompatible protocol change. Bumping the crate version or the control protocol version must not change an existing `NetworkId`. 3. **Secrets never leak.** Not into logs, not into `Debug`, not into status @@ -126,7 +126,7 @@ Keep these separate. Crossing them is the main thing to review for. the library; tests opt out explicitly at the top of each file. 5. **Bounds before allocation.** Frame lengths are checked against `Limits::max_frame_len` before a buffer is allocated. Every string, list and - queue has a limit in `crates/tsunagi/src/config.rs`. + queue has a limit in `crates/tsunagi-core/src/config.rs`. 6. **Failure is contained.** A bad signature, wrong secret, malformed packet or unknown version rejects one message or one session. It never stops another network and never stops the agent. There is no irreversible global error @@ -150,18 +150,18 @@ Keep these separate. Crossing them is the main thing to review for. | path | responsibility | |---------------------|----------------| -| `crates/tsunagi/src/identity/` | device identity; deterministic network space identity and derived keys | -| `crates/tsunagi/src/storage/` | `state.sqlite`, `cache.sqlite`, directory ownership lock | -| `crates/tsunagi/src/discovery.rs` | candidate sources; test and static backends | -| `crates/tsunagi/src/proto/` | framing, message formats, membership handshake | -| `crates/tsunagi/src/net.rs` | iroh endpoint adapter and observability snapshots | -| `crates/tsunagi/src/agent/` | agent lifecycle, per-network runtimes, sessions, events, status | -| `crates/tsunagi/src/state/` | signed records that outlive a session, their merge rules and address allocation | -| `crates/tsunagi/src/overlay/` | the one interface an agent owns: provisioning, the TUN, routing, source checks, broadcast host rules | -| `crates/tsunagi/src/dataplane/` | the protocol contract and the authenticated packet transport | -| `crates/tsunagi/src/dns/` | the DNS view of a network: zone, server, resolver publication | +| `crates/tsunagi-core/src/identity/` | device identity; deterministic network space identity and derived keys | +| `crates/tsunagi-core/src/storage/` | `state.sqlite`, `cache.sqlite`, directory ownership lock | +| `crates/tsunagi-core/src/discovery.rs` | candidate sources; test and static backends | +| `crates/tsunagi-core/src/proto/` | framing, message formats, membership handshake | +| `crates/tsunagi-core/src/net.rs` | iroh endpoint adapter and observability snapshots | +| `crates/tsunagi-core/src/agent/` | agent lifecycle, per-network runtimes, sessions, events, status | +| `crates/tsunagi-core/src/state/` | signed records that outlive a session, their merge rules and address allocation | +| `crates/tsunagi-core/src/overlay/` | the one interface an agent owns: provisioning, the TUN, routing, source checks, broadcast host rules | +| `crates/tsunagi-core/src/dataplane/` | the protocol contract and the authenticated packet transport | +| `crates/tsunagi-core/src/dns/` | the DNS view of a network: zone, server, resolver publication | | `crates/tsunagi-wg-quic/` | the `wg-quic` protocol: its keys, its announcement, its tunnels | -| `crates/tsunagi-cli/` | the command line agent; the only place that owns a runtime, a logger and signals | +| `crates/tsunagi/` | the command line agent, built as `tsng`; the only place that owns a runtime, a logger and signals | | `tests/` | integration tests; `tests/common/` is the shared harness | Add abstractions only at real substitution or testing boundaries. Do not add a diff --git a/Cargo.lock b/Cargo.lock index 5ff6d21..1529e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6234,6 +6234,27 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tsunagi" version = "0.1.0" +dependencies = [ + "anstream", + "anstyle", + "clap", + "iroh", + "netwatch", + "nix 0.31.3", + "simple-dns", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "tsunagi-core", + "tsunagi-tcp-tls", + "tsunagi-wg-quic", + "tsunagi-wireguard", +] + +[[package]] +name = "tsunagi-core" +version = "0.1.0" dependencies = [ "arc-swap", "bytes", @@ -6266,34 +6287,13 @@ dependencies = [ "tokio-stream", "tracing", "tracing-subscriber", - "tsunagi", + "tsunagi-core", "tun", "windows-sys 0.61.2", "zbus", "zeroize", ] -[[package]] -name = "tsunagi-cli" -version = "0.1.0" -dependencies = [ - "anstream", - "anstyle", - "clap", - "iroh", - "netwatch", - "nix 0.31.3", - "simple-dns", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "tsunagi", - "tsunagi-tcp-tls", - "tsunagi-wg-quic", - "tsunagi-wireguard", -] - [[package]] name = "tsunagi-tcp-tls" version = "0.1.0" @@ -6318,7 +6318,7 @@ dependencies = [ "tokio-rustls", "tracing", "tracing-subscriber", - "tsunagi", + "tsunagi-core", "x25519-dalek", "zeroize", ] @@ -6334,7 +6334,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tray-icon", - "tsunagi", + "tsunagi-core", "winit", ] @@ -6359,7 +6359,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "tsunagi", + "tsunagi-core", "zeroize", ] @@ -6383,7 +6383,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "tsunagi", + "tsunagi-core", "zeroize", ] diff --git a/README.md b/README.md index f37775f..3f75a72 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ A working library with **real iroh connections** and integration tests: overlay with deterministically derived addresses and optional IPv4, real tunnels carried over iroh, and address ownership enforced rather than believed; -- a **command line agent**, `tsunagi`, with a local control socket. +- a **command line agent**, `tsng`, with a local control socket. ### What it deliberately does **not** do @@ -71,8 +71,8 @@ On the first machine: ```bash cargo build --release -./target/release/tsunagi status # this device, the agent, and this host -./target/release/tsunagi network # the networks this device belongs to +./target/release/tsng status # this device, the agent, and this host +./target/release/tsng network # the networks this device belongs to ``` Two commands do the work, and they are separate on purpose: `up` runs the @@ -81,19 +81,19 @@ what it belongs to, at any time, while it runs. ```bash # Terminal one: the agent. It prints its endpoint id and then serves. -./target/release/tsunagi up +./target/release/tsng up # Terminal two: make a network. With no --secret it invents one and prints # it, along with the line to send the other machine. -./target/release/tsunagi join --network lab +./target/release/tsng join --network lab ``` On the second machine, start its agent and join the same network with the secret that was printed: ```bash -./target/release/tsunagi up -./target/release/tsunagi join --network lab --secret tsn1... +./target/release/tsng up +./target/release/tsng join --network lab --secret tsn1... ``` **Name and secret are enough for the first meeting.** Mainline DHT discovery @@ -141,7 +141,7 @@ wg-quic: tsun0 on 10.13.37.69/24 mtu 1280, 1/1 tunnel(s) established From another shell on either machine: ```bash -tsunagi status +tsng status ``` ```text @@ -170,8 +170,8 @@ ping 100.65.243.53 what they find, so `--ipv4-range` only matters for whoever starts the network: ```bash -tsunagi up --ipv4-range 10.44.0.0/16 -tsunagi up --ipv4-range none # no data plane at all +tsng up --ipv4-range 10.44.0.0/16 +tsng up --ipv4-range none # no data plane at all ``` One agent has one interface, so two of its networks cannot both use that @@ -193,7 +193,7 @@ assigns it to the interface itself. ## Levels -The command line is split the way the design is. A bare `tsunagi up` needs +The command line is split the way the design is. A bare `tsng up` needs only a network name and a secret; everything else sits under the level it belongs to, which `--help` shows as two sections: @@ -204,7 +204,7 @@ belongs to, which `--help` shows as two sections: their own settings (`-o key=value`, or `-o protocol:key=value`). ```bash -tsunagi protocols # what this build can carry packets with +tsng protocols # what this build can carry packets with ``` ```text @@ -223,7 +223,7 @@ A pair of peers uses a protocol they both have **at the same wire version**. That is not the software version: two peers on different builds carry traffic for each other for as long as the bytes between them have not changed. A peer with nothing in common keeps its control plane — messages and signed state -still flow — and simply has no data plane, which `tsunagi status` shows as a +still flow — and simply has no data plane, which `tsng status` shows as a session with no agreed protocol. ## Names @@ -233,12 +233,12 @@ each named after the network, so members can be reached by name instead of by address: ```bash -tsunagi up # DNS is already enabled +tsng up # DNS is already enabled dig @127.0.0.1 -p 5354 music.lab ``` -An explicit choice is remembered across restarts. `--no-dns` or `tsunagi dns off` -disables it; `--dns` or `tsunagi dns on` enables it again. Existing saved opt-outs +An explicit choice is remembered across restarts. `--no-dns` or `tsng dns off` +disables it; `--dns` or `tsng dns on` enables it again. Existing saved opt-outs remain respected. The `dns on` and `dns off` commands also take effect immediately on a running agent. `--dns` and `--no-dns` cannot be combined. @@ -299,7 +299,7 @@ polkit.addRule(function(action, subject) { RULE ``` -**Without it the server still runs** — `tsunagi status` prints where it is +**Without it the server still runs** — `tsng status` prints where it is listening and the exact `dig` line — so the automatic part is missing, not the feature. The refusal is said once rather than on every pass, and retried slowly, because nothing but a person will change it. @@ -318,7 +318,7 @@ remote peer could influence. That needs `CAP_NET_ADMIN`, granted once: ```bash -sudo setcap cap_net_admin+p /usr/local/bin/tsunagi +sudo setcap cap_net_admin+p /usr/local/bin/tsng ``` `+p` rather than `+ep`: the capability is then *permitted* but not @@ -328,7 +328,7 @@ allocation changes. Everything else, including every byte from the network, is handled with it lowered. `+ep` works too; the agent lowers it on the way in. -`tsunagi status` says which of these applies on the host it runs on, along +`tsng status` says which of these applies on the host it runs on, along with what the agent is doing. It grades each finding: **ok** for what works, **warn** for what the agent runs without and you can fix from the line it prints, **FAIL** for what it cannot work around. The words carry the grade as @@ -353,28 +353,28 @@ Every item takes the same shape, so there is nothing to remember: name it to see it, name it with a value to change it. ``` -tsunagi id everything about this device -tsunagi id hostname the name it answers to -tsunagi id hostname mango change it -tsunagi id key the key it signs with -tsunagi id key rotate replace that key +tsng id everything about this device +tsng id hostname the name it answers to +tsng id hostname mango change it +tsng id key the key it signs with +tsng id key rotate replace that key -tsunagi network the networks this device belongs to -tsunagi network join -n lab -s tsn1… join one; adds it to a running agent -tsunagi network join -n lab resume one this device has, or make it on the spot -tsunagi network stop lab stop serving it, keeping everything -tsunagi network start lab serve it again, from where it left off -tsunagi network leave lab give up the address and name, then forget it -tsunagi network secret the secret of each joined network -tsunagi network secret lab just that one, for copying -tsunagi network secret generate a fresh secret for a network that does not exist yet +tsng network the networks this device belongs to +tsng network join -n lab -s tsn1… join one; adds it to a running agent +tsng network join -n lab resume one this device has, or make it on the spot +tsng network stop lab stop serving it, keeping everything +tsng network start lab serve it again, from where it left off +tsng network leave lab give up the address and name, then forget it +tsng network secret the secret of each joined network +tsng network secret lab just that one, for copying +tsng network secret generate a fresh secret for a network that does not exist yet -tsunagi dns whether the local resolver is serving, and what -tsunagi dns on start it, now and after every restart -tsunagi dns off stop it, now and after every restart +tsng dns whether the local resolver is serving, and what +tsng dns on start it, now and after every restart +tsng dns off stop it, now and after every restart ``` -**A network without a secret makes one.** `tsunagi join --network lab` +**A network without a secret makes one.** `tsng join --network lab` resolves a bare name in the obvious way: if this device is already in exactly one network called `lab`, that one — so the name alone resumes what you have; if it is in none, a fresh random secret, printed in full with the @@ -386,11 +386,11 @@ joined `lab` (k2on43wadbi5x267vp6z3ogkm7nbjedfdoxyauxhtxhwqrprylba) Run this on the other machine: - tsunagi join --network lab --secret tsn1u7c… + tsng join --network lab --secret tsn1u7c… Its agent has to be running. If it is not: - tsunagi up --peer 91e83a6e2b7a… + tsng up --peer 91e83a6e2b7a… ``` Two networks of one name is a thing that happens — a mistyped secret makes @@ -410,11 +410,11 @@ Asking for it is deliberate, because these reports get pasted into chats. **`up` takes no network at all** if you would rather decide later: it brings up the agent and whatever it is already configured for, and waits. -That is the shape of a daemon in one terminal and `tsunagi network join` +That is the shape of a daemon in one terminal and `tsng network join` in another. `network join` is also the answer to a question `up` cannot: a state -directory belongs to one live agent, so a second `tsunagi up` cannot add a +directory belongs to one live agent, so a second `tsng up` cannot add a network to the one already running. This adds it over the control socket and it starts at once. With no agent running it is written to the configuration and starts with the next `up`. @@ -477,11 +477,11 @@ The original IP packet, including its DF bit, is preserved. See |---|---|---| | `setcap cap_net_admin+p` | ordinary user, one capability | recommended: nothing to prepare, nothing left behind. Lost on every rebuild or copy of the binary. | | systemd service | `User=`, `AmbientCapabilities=CAP_NET_ADMIN` | the same, for an installed service | -| `sudo tsunagi up` | root | everything works, nothing is isolated | +| `sudo tsng up` | root | everything works, nothing is isolated | | macOS root LaunchDaemon | root | the native macOS deployment; see below | | `--no-tun` | ordinary user, no capabilities | tunnels run and handshake, traffic never reaches the OS | -On Windows, put `wintun.dll` beside the executable and start `tsunagi up` +On Windows, put `wintun.dll` beside the executable and start `tsng up` from PowerShell or Command Prompt opened with **Run as administrator**. Run commands controlling that agent (`join`, `status`, `dns`, `network`) as the same Windows user with the same elevation. If Windows denies access @@ -498,7 +498,7 @@ grant like Linux, and a `utun` plus `/etc/resolver` are root-only. The kernel assigns the interface its `utunN` name, so the name derived from the network id is only a request and the real one is adopted once the interface is up. -Run it as root — `sudo tsunagi up` for a quick start, or install it as a root +Run it as root — `sudo tsng up` for a quick start, or install it as a root **LaunchDaemon** for the native always-on deployment. As a daemon the agent runs unprivileged commands (`join`, `status`, `dns`, `network`) over its control socket like any other platform; the socket lives at `/var/run/tsunagi/agent.sock` @@ -613,10 +613,10 @@ paths. It supports `255.255.255.255` and the overlay subnet's broadcast address; received broadcasts never trigger another fanout. ```sh -tsunagi join -n games --no-broadcast -tsunagi join -n games --broadcast -tsunagi network broadcast games off -tsunagi network broadcast games on +tsng join -n games --no-broadcast +tsng join -n games --broadcast +tsng network broadcast games off +tsng network broadcast games on ``` The choice persists across restarts and a plain `join`. It can be changed while @@ -681,8 +681,8 @@ directories by default; `--state-dir` and `--cache-dir` override them. One state directory belongs to one live agent, enforced with a real OS file lock rather than an existence check. That directory *is* the identity: one agent, one device key, one interface, and as many networks as you like on it -— `tsunagi network join` adds them to the agent that is already running, -which is why a second `tsunagi up` on the same directory is refused rather +— `tsng network join` adds them to the agent that is already running, +which is why a second `tsng up` on the same directory is refused rather than made to work. A second agent on the same host is a second identity, and needs everything of its own: its own state and cache directories, its own interface name and its own overlay range. The library owns no globals, so @@ -693,9 +693,9 @@ several of them run side by side in one process as readily as in one host. Membership outlives a session, so it also has to be possible to end it. ```bash -tsunagi network # what this device belongs to -tsunagi network leave lab # give up the address and the name, then forget it -tsunagi wipe --yes # remove everything and be a stranger again +tsng network # what this device belongs to +tsng network leave lab # give up the address and the name, then forget it +tsng wipe --yes # remove everything and be a stranger again ``` **Stopping is not leaving.** `stop` closes this network's sessions, takes diff --git a/crates/tsunagi-cli/Cargo.toml b/crates/tsunagi-cli/Cargo.toml deleted file mode 100644 index 00253ad..0000000 --- a/crates/tsunagi-cli/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "tsunagi-cli" -version = "0.1.0" -description = "The tsunagi command line agent." -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true - -[[bin]] -name = "tsunagi" -path = "src/main.rs" -# The binary and the library share a name, which `cargo doc` cannot map to -# two directories. The command line is documented by `--help`, not rustdoc, -# so the library keeps the name and this opts out. -doc = false - -[dependencies] -tsunagi = { path = "../tsunagi", version = "0.1.0", features = ["tun-device", "dns-publish"] } -tsunagi-wireguard = { path = "../tsunagi-wireguard", version = "0.1.0" } -tsunagi-wg-quic = { path = "../tsunagi-wg-quic", version = "0.1.0" } -tsunagi-tcp-tls = { path = "../tsunagi-tcp-tls", version = "0.1.0" } -iroh.workspace = true -tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "signal"] } -tracing.workspace = true -tracing-subscriber.workspace = true -clap = { version = "4.5", features = ["derive", "env"] } -# Already in the tree through clap. `anstream` strips the escapes when stdout -# is not a terminal and turns on virtual terminal processing on Windows, so -# colour is never written where it would show up as rubbish. -anstream = "1.0" -anstyle = "1.0" -# Local interface addresses, for the diagnostics in `status`. -netwatch = "0.19.3" - -# Resolving a group name to its gid for `up --control-group`, through a safe -# wrapper (no unsafe of our own). Unix only. -[target.'cfg(unix)'.dependencies] -nix = { version = "0.31", features = ["user"] } - -[dev-dependencies] -tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "process"] } -tempfile.workspace = true -simple-dns = "0.12" - -[lints] -workspace = true diff --git a/crates/tsunagi-cli/tests/identity.rs b/crates/tsunagi-cli/tests/identity.rs deleted file mode 100644 index 32cf5fe..0000000 --- a/crates/tsunagi-cli/tests/identity.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! What the command line says about this device, against a running agent. -//! -//! These drive the real binary, which is why they live beside it rather -//! than with the library's own tests: the library cannot depend on a binary -//! built from a crate that depends on it. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -use tempfile::TempDir; -use tsunagi::Agent; -use tsunagi::config::{AgentConfig, StoragePaths, TransportPolicy}; - -/// Asking who this device is must not need the directory lock. -/// -/// The lock belongs to the one agent allowed to *write* the state. `id` only -/// reads, so making it take the lock would mean the question could never be -/// answered while an agent was running — which is exactly when you want to -/// ask it. -#[tokio::test] -async fn identity_can_be_read_while_an_agent_holds_the_directory() { - let dir = TempDir::new().unwrap(); - - // Hold the directory the way a running agent does. - let agent = Agent::spawn( - AgentConfig::new(StoragePaths::under(dir.path())) - .with_transport(TransportPolicy::LocalOnly) - .with_loopback_bind(), - ) - .await - .unwrap(); - let expected = agent.endpoint_id().to_string(); - - let output = tokio::process::Command::new(env!("CARGO_BIN_EXE_tsunagi")) - .arg("id") - // The same layout `StoragePaths::under` gives the agent above. - .arg("--state-dir") - .arg(dir.path().join("state")) - .arg("--cache-dir") - .arg(dir.path().join("cache")) - .output() - .await - .unwrap(); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - output.status.success(), - "`id` failed while an agent was running:\n{stderr}" - ); - assert!( - stdout.contains(&expected), - "expected {expected} in:\n{stdout}" - ); - - agent.shutdown().await; -} diff --git a/crates/tsunagi-core/Cargo.toml b/crates/tsunagi-core/Cargo.toml new file mode 100644 index 0000000..ab50eb1 --- /dev/null +++ b/crates/tsunagi-core/Cargo.toml @@ -0,0 +1,113 @@ +[package] +name = "tsunagi-core" +version = "0.1.0" +description = "Small private mesh networks: persistent agent identity, deterministic network spaces, an iroh control plane, signed state and a local DNS view." +readme = "../../README.md" +keywords = ["mesh", "p2p", "iroh", "networking"] +categories = ["network-programming"] +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +name = "tsunagi" + +[features] +default = [] +# A real TUN device, so a plugin can carry actual IP traffic. On Linux it +# needs CAP_NET_ADMIN; on Windows the process must run elevated and +# `wintun.dll` must be reachable. Without either the in-memory device serves +# tests. The platform-specific dependencies are declared per target, so +# enabling this only pulls what the target it is built for actually uses. +tun-device = [ + "dep:tun", + "dep:rtnetlink", + "dep:caps", + "dep:futures-util", +] +# Telling the operating system where to send its DNS questions. systemd-resolved +# on Linux, the Name Resolution Policy Table on Windows; the zone and the +# server work without it either way. +dns-publish = ["dep:zbus"] +# The test harness in `testing`, for this crate's own tests and for a +# protocol crate's. Not on by default: it is only of use to a test. +testing = ["dep:tempfile", "dep:tracing-subscriber"] + +[dependencies] +arc-swap = "1.9" +iroh.workspace = true +# `net` for the local control interface: a Unix socket on Unix, a named pipe +# on Windows. Both live behind the same `net` feature. +tokio = { workspace = true, features = ["net"] } +tokio-stream = "0.1" +serde.workspace = true +postcard.workspace = true +bytes.workspace = true +thiserror.workspace = true +tracing.workspace = true +hex.workspace = true +data-encoding.workspace = true +rusqlite = { version = "0.40", features = ["bundled"] } +hkdf = "0.13" +hmac = "0.13" +sha2 = "0.11" +subtle = "2.6" +zeroize = { version = "1.9", features = ["derive"] } +rand = "0.10" +# Already in the tree through iroh. A packet codec, not a DNS server: the +# zone logic is ours and a full server framework would be a large dependency +# for answering A records from memory. +simple-dns = "0.12" +# The IANA top-level domain list, compiled in: one function, no +# dependencies, no network. Used only to warn that a zone name shadows a +# real public domain, never to refuse one. +tld = "2.40" +fs4 = { version = "1.1", features = ["sync"] } +directories = "6.0" +# The real system hostname, without a libc call of our own: this crate is +# forbidden `unsafe` and will not make one. +gethostname = "1.1" +netwatch = "0.19.3" +netdev = "0.46" +mainline = "8.0" +futures-lite = "2.6" +tun = { version = "0.8", features = ["async"], optional = true } +# Only for the `testing` harness, which a protocol crate's tests use too. +tempfile = { version = "3.24", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } + +# Linux-only interface provisioning. `rtnetlink` configures the interface in +# process, so no `ip` invocation is ever needed; `caps` keeps CAP_NET_ADMIN +# out of the effective set except during the moments it is used. +[target.'cfg(target_os = "linux")'.dependencies] +rtnetlink = { version = "0.23", optional = true } +# Pure Rust D-Bus, no libdbus to link against. `tokio` rather than the +# default reactor, because the agent brings its own. +zbus = { version = "5.19", default-features = false, features = ["tokio"], optional = true } +caps = { version = "0.5", optional = true } +futures-util = { version = "0.3", default-features = false, optional = true } + +# Windows-only interface provisioning. The Wintun adapter itself is created +# and driven through the `tun` dependency, whose safe wrapper is the only way +# in — loading `wintun.dll` is an `unsafe` call this crate forbids. `netdev` +# (already in the tree through `netwatch`) reads the interface table through a +# safe wrapper, which is how the provisioner observes an adapter by name +# before deciding what to change. Creating an adapter needs the process to be +# elevated and `wintun.dll` to be reachable at run time. +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.61", features = [ + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Foundation", +] } + +[dev-dependencies] +# Its own tests use the harness it publishes. +tsunagi-core = { path = ".", features = ["testing"] } +tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "process", "test-util"] } +tempfile.workspace = true +tracing-subscriber.workspace = true + +[lints] +workspace = true diff --git a/crates/tsunagi/src/agent/events.rs b/crates/tsunagi-core/src/agent/events.rs similarity index 100% rename from crates/tsunagi/src/agent/events.rs rename to crates/tsunagi-core/src/agent/events.rs diff --git a/crates/tsunagi/src/agent/mod.rs b/crates/tsunagi-core/src/agent/mod.rs similarity index 99% rename from crates/tsunagi/src/agent/mod.rs rename to crates/tsunagi-core/src/agent/mod.rs index 07c92fe..546d642 100644 --- a/crates/tsunagi/src/agent/mod.rs +++ b/crates/tsunagi-core/src/agent/mod.rs @@ -582,7 +582,7 @@ impl Agent { "`{name}` is already configured with a different secret, as {}. \ Joining with this one adds a second network under the same name; \ they share nothing. Check the secret, or use \ - `tsunagi network secret` \ + `tsng network secret` \ to see which is which.", other.network_id ); diff --git a/crates/tsunagi/src/agent/network.rs b/crates/tsunagi-core/src/agent/network.rs similarity index 99% rename from crates/tsunagi/src/agent/network.rs rename to crates/tsunagi-core/src/agent/network.rs index 0def324..ad8f089 100644 --- a/crates/tsunagi/src/agent/network.rs +++ b/crates/tsunagi-core/src/agent/network.rs @@ -1218,7 +1218,7 @@ impl Runtime { network = %self.network_id, exit_node = %via.fmt_short(), "the exit node is gone or no longer offers to be one; internet traffic stays \ - routed through it and is dropped until it returns or `tsunagi exit-node off`" + routed through it and is dropped until it returns or `tsng exit-node off`" ); } } diff --git a/crates/tsunagi/src/agent/session.rs b/crates/tsunagi-core/src/agent/session.rs similarity index 100% rename from crates/tsunagi/src/agent/session.rs rename to crates/tsunagi-core/src/agent/session.rs diff --git a/crates/tsunagi/src/agent/shutdown.rs b/crates/tsunagi-core/src/agent/shutdown.rs similarity index 100% rename from crates/tsunagi/src/agent/shutdown.rs rename to crates/tsunagi-core/src/agent/shutdown.rs diff --git a/crates/tsunagi/src/agent/status.rs b/crates/tsunagi-core/src/agent/status.rs similarity index 100% rename from crates/tsunagi/src/agent/status.rs rename to crates/tsunagi-core/src/agent/status.rs diff --git a/crates/tsunagi/src/config.rs b/crates/tsunagi-core/src/config.rs similarity index 100% rename from crates/tsunagi/src/config.rs rename to crates/tsunagi-core/src/config.rs diff --git a/crates/tsunagi/src/dataplane/mod.rs b/crates/tsunagi-core/src/dataplane/mod.rs similarity index 100% rename from crates/tsunagi/src/dataplane/mod.rs rename to crates/tsunagi-core/src/dataplane/mod.rs diff --git a/crates/tsunagi/src/dataplane/relay.rs b/crates/tsunagi-core/src/dataplane/relay.rs similarity index 100% rename from crates/tsunagi/src/dataplane/relay.rs rename to crates/tsunagi-core/src/dataplane/relay.rs diff --git a/crates/tsunagi/src/dataplane/relay_tests.rs b/crates/tsunagi-core/src/dataplane/relay_tests.rs similarity index 100% rename from crates/tsunagi/src/dataplane/relay_tests.rs rename to crates/tsunagi-core/src/dataplane/relay_tests.rs diff --git a/crates/tsunagi/src/dataplane/routing/envelope.rs b/crates/tsunagi-core/src/dataplane/routing/envelope.rs similarity index 100% rename from crates/tsunagi/src/dataplane/routing/envelope.rs rename to crates/tsunagi-core/src/dataplane/routing/envelope.rs diff --git a/crates/tsunagi/src/dataplane/routing/flow.rs b/crates/tsunagi-core/src/dataplane/routing/flow.rs similarity index 100% rename from crates/tsunagi/src/dataplane/routing/flow.rs rename to crates/tsunagi-core/src/dataplane/routing/flow.rs diff --git a/crates/tsunagi/src/dataplane/routing/mod.rs b/crates/tsunagi-core/src/dataplane/routing/mod.rs similarity index 100% rename from crates/tsunagi/src/dataplane/routing/mod.rs rename to crates/tsunagi-core/src/dataplane/routing/mod.rs diff --git a/crates/tsunagi/src/dataplane/transport/fragments.rs b/crates/tsunagi-core/src/dataplane/transport/fragments.rs similarity index 100% rename from crates/tsunagi/src/dataplane/transport/fragments.rs rename to crates/tsunagi-core/src/dataplane/transport/fragments.rs diff --git a/crates/tsunagi/src/dataplane/transport/iroh_link.rs b/crates/tsunagi-core/src/dataplane/transport/iroh_link.rs similarity index 100% rename from crates/tsunagi/src/dataplane/transport/iroh_link.rs rename to crates/tsunagi-core/src/dataplane/transport/iroh_link.rs diff --git a/crates/tsunagi/src/dataplane/transport/mod.rs b/crates/tsunagi-core/src/dataplane/transport/mod.rs similarity index 100% rename from crates/tsunagi/src/dataplane/transport/mod.rs rename to crates/tsunagi-core/src/dataplane/transport/mod.rs diff --git a/crates/tsunagi/src/dataplane/tunnel.rs b/crates/tsunagi-core/src/dataplane/tunnel.rs similarity index 100% rename from crates/tsunagi/src/dataplane/tunnel.rs rename to crates/tsunagi-core/src/dataplane/tunnel.rs diff --git a/crates/tsunagi/src/discovery.rs b/crates/tsunagi-core/src/discovery.rs similarity index 100% rename from crates/tsunagi/src/discovery.rs rename to crates/tsunagi-core/src/discovery.rs diff --git a/crates/tsunagi/src/discovery/mainline.rs b/crates/tsunagi-core/src/discovery/mainline.rs similarity index 100% rename from crates/tsunagi/src/discovery/mainline.rs rename to crates/tsunagi-core/src/discovery/mainline.rs diff --git a/crates/tsunagi/src/discovery/worker.rs b/crates/tsunagi-core/src/discovery/worker.rs similarity index 100% rename from crates/tsunagi/src/discovery/worker.rs rename to crates/tsunagi-core/src/discovery/worker.rs diff --git a/crates/tsunagi/src/dns/mod.rs b/crates/tsunagi-core/src/dns/mod.rs similarity index 100% rename from crates/tsunagi/src/dns/mod.rs rename to crates/tsunagi-core/src/dns/mod.rs diff --git a/crates/tsunagi/src/dns/publish/macos.rs b/crates/tsunagi-core/src/dns/publish/macos.rs similarity index 100% rename from crates/tsunagi/src/dns/publish/macos.rs rename to crates/tsunagi-core/src/dns/publish/macos.rs diff --git a/crates/tsunagi/src/dns/publish/mock.rs b/crates/tsunagi-core/src/dns/publish/mock.rs similarity index 100% rename from crates/tsunagi/src/dns/publish/mock.rs rename to crates/tsunagi-core/src/dns/publish/mock.rs diff --git a/crates/tsunagi/src/dns/publish/mod.rs b/crates/tsunagi-core/src/dns/publish/mod.rs similarity index 100% rename from crates/tsunagi/src/dns/publish/mod.rs rename to crates/tsunagi-core/src/dns/publish/mod.rs diff --git a/crates/tsunagi/src/dns/publish/resolved.rs b/crates/tsunagi-core/src/dns/publish/resolved.rs similarity index 100% rename from crates/tsunagi/src/dns/publish/resolved.rs rename to crates/tsunagi-core/src/dns/publish/resolved.rs diff --git a/crates/tsunagi/src/dns/publish/unsupported.rs b/crates/tsunagi-core/src/dns/publish/unsupported.rs similarity index 100% rename from crates/tsunagi/src/dns/publish/unsupported.rs rename to crates/tsunagi-core/src/dns/publish/unsupported.rs diff --git a/crates/tsunagi/src/dns/publish/windows.rs b/crates/tsunagi-core/src/dns/publish/windows.rs similarity index 100% rename from crates/tsunagi/src/dns/publish/windows.rs rename to crates/tsunagi-core/src/dns/publish/windows.rs diff --git a/crates/tsunagi/src/dns/server.rs b/crates/tsunagi-core/src/dns/server.rs similarity index 100% rename from crates/tsunagi/src/dns/server.rs rename to crates/tsunagi-core/src/dns/server.rs diff --git a/crates/tsunagi/src/dns/zone.rs b/crates/tsunagi-core/src/dns/zone.rs similarity index 100% rename from crates/tsunagi/src/dns/zone.rs rename to crates/tsunagi-core/src/dns/zone.rs diff --git a/crates/tsunagi/src/error.rs b/crates/tsunagi-core/src/error.rs similarity index 100% rename from crates/tsunagi/src/error.rs rename to crates/tsunagi-core/src/error.rs diff --git a/crates/tsunagi/src/identity/mod.rs b/crates/tsunagi-core/src/identity/mod.rs similarity index 100% rename from crates/tsunagi/src/identity/mod.rs rename to crates/tsunagi-core/src/identity/mod.rs diff --git a/crates/tsunagi/src/identity/network.rs b/crates/tsunagi-core/src/identity/network.rs similarity index 100% rename from crates/tsunagi/src/identity/network.rs rename to crates/tsunagi-core/src/identity/network.rs diff --git a/crates/tsunagi/src/ipc/mod.rs b/crates/tsunagi-core/src/ipc/mod.rs similarity index 99% rename from crates/tsunagi/src/ipc/mod.rs rename to crates/tsunagi-core/src/ipc/mod.rs index 7862d82..b54f1b4 100644 --- a/crates/tsunagi/src/ipc/mod.rs +++ b/crates/tsunagi-core/src/ipc/mod.rs @@ -214,7 +214,7 @@ pub enum Request { /// /// Asked of the running agent because that is the only way to add a /// network to an agent that is already up: the state directory belongs - /// to one live agent, so a second `tsunagi up` cannot. + /// to one live agent, so a second `tsng up` cannot. Join { /// The network name. name: String, @@ -764,7 +764,7 @@ where /// /// A local answer comes from memory, so anything this slow means the agent is /// wedged rather than busy. Saying so beats waiting: unbounded, one wedged -/// runtime leaves `tsunagi status` hanging with nothing on screen and no way +/// runtime leaves `tsng status` hanging with nothing on screen and no way /// out but Ctrl-C. pub const EXCHANGE_TIMEOUT: Duration = Duration::from_secs(5); diff --git a/crates/tsunagi/src/ipc/unix.rs b/crates/tsunagi-core/src/ipc/unix.rs similarity index 100% rename from crates/tsunagi/src/ipc/unix.rs rename to crates/tsunagi-core/src/ipc/unix.rs diff --git a/crates/tsunagi/src/ipc/windows.rs b/crates/tsunagi-core/src/ipc/windows.rs similarity index 99% rename from crates/tsunagi/src/ipc/windows.rs rename to crates/tsunagi-core/src/ipc/windows.rs index da842fa..ee25fb9 100644 --- a/crates/tsunagi/src/ipc/windows.rs +++ b/crates/tsunagi-core/src/ipc/windows.rs @@ -210,7 +210,7 @@ impl Drop for ControlSocket { /// Accepts one client at a time, keeping a fresh instance ready for the next. /// /// A named pipe server instance serves a single client, so a new instance is -/// created as soon as one is taken — otherwise a second `tsunagi status` while +/// created as soon as one is taken — otherwise a second `tsng status` while /// the first is mid-flight would find nothing listening. async fn serve( name: String, @@ -294,7 +294,7 @@ pub(crate) async fn connect(path: &Path) -> Result { format!( "the agent's control pipe exists, but Windows denied access. \ Run this command as the same Windows user and with the same \ - elevation as `tsunagi up` (use an administrator terminal if \ + elevation as `tsng up` (use an administrator terminal if \ the agent is elevated). Windows error: {err}" ), ), diff --git a/crates/tsunagi/src/lib.rs b/crates/tsunagi-core/src/lib.rs similarity index 100% rename from crates/tsunagi/src/lib.rs rename to crates/tsunagi-core/src/lib.rs diff --git a/crates/tsunagi/src/net.rs b/crates/tsunagi-core/src/net.rs similarity index 100% rename from crates/tsunagi/src/net.rs rename to crates/tsunagi-core/src/net.rs diff --git a/crates/tsunagi/src/overlay/broadcast.rs b/crates/tsunagi-core/src/overlay/broadcast.rs similarity index 100% rename from crates/tsunagi/src/overlay/broadcast.rs rename to crates/tsunagi-core/src/overlay/broadcast.rs diff --git a/crates/tsunagi/src/overlay/config.rs b/crates/tsunagi-core/src/overlay/config.rs similarity index 100% rename from crates/tsunagi/src/overlay/config.rs rename to crates/tsunagi-core/src/overlay/config.rs diff --git a/crates/tsunagi/src/overlay/exit.rs b/crates/tsunagi-core/src/overlay/exit.rs similarity index 100% rename from crates/tsunagi/src/overlay/exit.rs rename to crates/tsunagi-core/src/overlay/exit.rs diff --git a/crates/tsunagi/src/overlay/hostrules/exit.rs b/crates/tsunagi-core/src/overlay/hostrules/exit.rs similarity index 100% rename from crates/tsunagi/src/overlay/hostrules/exit.rs rename to crates/tsunagi-core/src/overlay/hostrules/exit.rs diff --git a/crates/tsunagi/src/overlay/hostrules/linux.rs b/crates/tsunagi-core/src/overlay/hostrules/linux.rs similarity index 100% rename from crates/tsunagi/src/overlay/hostrules/linux.rs rename to crates/tsunagi-core/src/overlay/hostrules/linux.rs diff --git a/crates/tsunagi/src/overlay/hostrules/linux_exit.rs b/crates/tsunagi-core/src/overlay/hostrules/linux_exit.rs similarity index 100% rename from crates/tsunagi/src/overlay/hostrules/linux_exit.rs rename to crates/tsunagi-core/src/overlay/hostrules/linux_exit.rs diff --git a/crates/tsunagi/src/overlay/hostrules/macos.rs b/crates/tsunagi-core/src/overlay/hostrules/macos.rs similarity index 100% rename from crates/tsunagi/src/overlay/hostrules/macos.rs rename to crates/tsunagi-core/src/overlay/hostrules/macos.rs diff --git a/crates/tsunagi/src/overlay/hostrules/mod.rs b/crates/tsunagi-core/src/overlay/hostrules/mod.rs similarity index 100% rename from crates/tsunagi/src/overlay/hostrules/mod.rs rename to crates/tsunagi-core/src/overlay/hostrules/mod.rs diff --git a/crates/tsunagi/src/overlay/hostrules/windows.rs b/crates/tsunagi-core/src/overlay/hostrules/windows.rs similarity index 99% rename from crates/tsunagi/src/overlay/hostrules/windows.rs rename to crates/tsunagi-core/src/overlay/hostrules/windows.rs index 522a238..e9ce2c0 100644 --- a/crates/tsunagi/src/overlay/hostrules/windows.rs +++ b/crates/tsunagi-core/src/overlay/hostrules/windows.rs @@ -90,7 +90,7 @@ fn classify(what: &str, text: &str) -> String { || lower.contains("run as administrator") || lower.contains("permissiondenied") { - format!("{what}: Windows refused; run tsunagi from an elevated (administrator) process") + format!("{what}: Windows refused; run tsng from an elevated (administrator) process") } else if lower.contains("is not recognized") || lower.contains("commandnotfound") { format!("{what}: the NetTCPIP/NetSecurity PowerShell modules are not available: {text}") } else if text.is_empty() { diff --git a/crates/tsunagi/src/overlay/interface.rs b/crates/tsunagi-core/src/overlay/interface.rs similarity index 100% rename from crates/tsunagi/src/overlay/interface.rs rename to crates/tsunagi-core/src/overlay/interface.rs diff --git a/crates/tsunagi/src/overlay/mod.rs b/crates/tsunagi-core/src/overlay/mod.rs similarity index 100% rename from crates/tsunagi/src/overlay/mod.rs rename to crates/tsunagi-core/src/overlay/mod.rs diff --git a/crates/tsunagi/src/overlay/packet.rs b/crates/tsunagi-core/src/overlay/packet.rs similarity index 100% rename from crates/tsunagi/src/overlay/packet.rs rename to crates/tsunagi-core/src/overlay/packet.rs diff --git a/crates/tsunagi/src/overlay/provision/factory.rs b/crates/tsunagi-core/src/overlay/provision/factory.rs similarity index 100% rename from crates/tsunagi/src/overlay/provision/factory.rs rename to crates/tsunagi-core/src/overlay/provision/factory.rs diff --git a/crates/tsunagi/src/overlay/provision/linux.rs b/crates/tsunagi-core/src/overlay/provision/linux.rs similarity index 100% rename from crates/tsunagi/src/overlay/provision/linux.rs rename to crates/tsunagi-core/src/overlay/provision/linux.rs diff --git a/crates/tsunagi/src/overlay/provision/macos.rs b/crates/tsunagi-core/src/overlay/provision/macos.rs similarity index 100% rename from crates/tsunagi/src/overlay/provision/macos.rs rename to crates/tsunagi-core/src/overlay/provision/macos.rs diff --git a/crates/tsunagi/src/overlay/provision/mock.rs b/crates/tsunagi-core/src/overlay/provision/mock.rs similarity index 100% rename from crates/tsunagi/src/overlay/provision/mock.rs rename to crates/tsunagi-core/src/overlay/provision/mock.rs diff --git a/crates/tsunagi/src/overlay/provision/mod.rs b/crates/tsunagi-core/src/overlay/provision/mod.rs similarity index 100% rename from crates/tsunagi/src/overlay/provision/mod.rs rename to crates/tsunagi-core/src/overlay/provision/mod.rs diff --git a/crates/tsunagi/src/overlay/provision/privilege.rs b/crates/tsunagi-core/src/overlay/provision/privilege.rs similarity index 96% rename from crates/tsunagi/src/overlay/provision/privilege.rs rename to crates/tsunagi-core/src/overlay/provision/privilege.rs index a8b9ef2..554d45b 100644 --- a/crates/tsunagi/src/overlay/provision/privilege.rs +++ b/crates/tsunagi-core/src/overlay/provision/privilege.rs @@ -15,7 +15,7 @@ //! Grant it with: //! //! ```text -//! sudo setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsunagi +//! sudo setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsng //! ``` //! //! `+p` rather than `+ep`: with `+p` the capability is permitted but not @@ -171,7 +171,7 @@ mod linux_impl { caps::raise(None, CapSet::Effective, Capability::CAP_NET_ADMIN).map_err(|err| { OverlayError::Unavailable(format!( "cannot raise CAP_NET_ADMIN: {err}. {}", - Privilege::how_to_grant("tsunagi") + Privilege::how_to_grant("tsng") )) })?; Ok(Self { raised: true }) @@ -204,10 +204,8 @@ mod tests { #[test] fn the_grant_instructions_name_the_program() { - let text = Privilege::how_to_grant("/usr/local/bin/tsunagi"); - assert!( - text.contains("setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsunagi") - ); + let text = Privilege::how_to_grant("/usr/local/bin/tsng"); + assert!(text.contains("setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsng")); assert!(text.contains("--no-tun"), "the fallback is offered too"); } diff --git a/crates/tsunagi/src/overlay/provision/unsupported.rs b/crates/tsunagi-core/src/overlay/provision/unsupported.rs similarity index 100% rename from crates/tsunagi/src/overlay/provision/unsupported.rs rename to crates/tsunagi-core/src/overlay/provision/unsupported.rs diff --git a/crates/tsunagi/src/overlay/provision/windows.rs b/crates/tsunagi-core/src/overlay/provision/windows.rs similarity index 100% rename from crates/tsunagi/src/overlay/provision/windows.rs rename to crates/tsunagi-core/src/overlay/provision/windows.rs diff --git a/crates/tsunagi/src/overlay/router.rs b/crates/tsunagi-core/src/overlay/router.rs similarity index 100% rename from crates/tsunagi/src/overlay/router.rs rename to crates/tsunagi-core/src/overlay/router.rs diff --git a/crates/tsunagi/src/overlay/tun.rs b/crates/tsunagi-core/src/overlay/tun.rs similarity index 98% rename from crates/tsunagi/src/overlay/tun.rs rename to crates/tsunagi-core/src/overlay/tun.rs index e615a8e..1f5bb2e 100644 --- a/crates/tsunagi/src/overlay/tun.rs +++ b/crates/tsunagi-core/src/overlay/tun.rs @@ -483,15 +483,15 @@ mod system { if denied { let hint = if cfg!(target_os = "windows") { "Open PowerShell or Command Prompt with 'Run as administrator' and \ - start `tsunagi up` there." + start `tsng up` there." } else if cfg!(target_os = "linux") { - "Start `tsunagi up` as root or grant CAP_NET_ADMIN with \ + "Start `tsng up` as root or grant CAP_NET_ADMIN with \ `sudo setcap cap_net_admin,cap_net_bind_service+p /path/to/tsunagi`." } else if cfg!(target_os = "macos") { - "Creating a utun interface needs root on macOS; start `tsunagi up` \ + "Creating a utun interface needs root on macOS; start `tsng up` \ with `sudo`, or install it as a root LaunchDaemon." } else { - "Start `tsunagi up` with the privileges required to create a TUN interface." + "Start `tsng up` with the privileges required to create a TUN interface." }; return OverlayError::Unavailable(format!( "permission denied while creating the TUN interface `{name}`. \ @@ -539,7 +539,7 @@ mod system { let message = open_error("tsun0", error).to_string(); assert!(message.contains("permission denied"), "{message}"); assert!(message.contains("tsun0"), "{message}"); - assert!(message.contains("tsunagi up"), "{message}"); + assert!(message.contains("tsng up"), "{message}"); #[cfg(target_os = "windows")] assert!(message.contains("Run as administrator"), "{message}"); #[cfg(target_os = "linux")] diff --git a/crates/tsunagi/src/proto/frame.rs b/crates/tsunagi-core/src/proto/frame.rs similarity index 100% rename from crates/tsunagi/src/proto/frame.rs rename to crates/tsunagi-core/src/proto/frame.rs diff --git a/crates/tsunagi/src/proto/handshake.rs b/crates/tsunagi-core/src/proto/handshake.rs similarity index 100% rename from crates/tsunagi/src/proto/handshake.rs rename to crates/tsunagi-core/src/proto/handshake.rs diff --git a/crates/tsunagi/src/proto/message.rs b/crates/tsunagi-core/src/proto/message.rs similarity index 100% rename from crates/tsunagi/src/proto/message.rs rename to crates/tsunagi-core/src/proto/message.rs diff --git a/crates/tsunagi/src/proto/mod.rs b/crates/tsunagi-core/src/proto/mod.rs similarity index 100% rename from crates/tsunagi/src/proto/mod.rs rename to crates/tsunagi-core/src/proto/mod.rs diff --git a/crates/tsunagi/src/state/allocator.rs b/crates/tsunagi-core/src/state/allocator.rs similarity index 100% rename from crates/tsunagi/src/state/allocator.rs rename to crates/tsunagi-core/src/state/allocator.rs diff --git a/crates/tsunagi/src/state/mod.rs b/crates/tsunagi-core/src/state/mod.rs similarity index 100% rename from crates/tsunagi/src/state/mod.rs rename to crates/tsunagi-core/src/state/mod.rs diff --git a/crates/tsunagi/src/storage/cache.rs b/crates/tsunagi-core/src/storage/cache.rs similarity index 100% rename from crates/tsunagi/src/storage/cache.rs rename to crates/tsunagi-core/src/storage/cache.rs diff --git a/crates/tsunagi/src/storage/lock.rs b/crates/tsunagi-core/src/storage/lock.rs similarity index 100% rename from crates/tsunagi/src/storage/lock.rs rename to crates/tsunagi-core/src/storage/lock.rs diff --git a/crates/tsunagi/src/storage/mod.rs b/crates/tsunagi-core/src/storage/mod.rs similarity index 100% rename from crates/tsunagi/src/storage/mod.rs rename to crates/tsunagi-core/src/storage/mod.rs diff --git a/crates/tsunagi/src/storage/state.rs b/crates/tsunagi-core/src/storage/state.rs similarity index 100% rename from crates/tsunagi/src/storage/state.rs rename to crates/tsunagi-core/src/storage/state.rs diff --git a/crates/tsunagi/src/task.rs b/crates/tsunagi-core/src/task.rs similarity index 100% rename from crates/tsunagi/src/task.rs rename to crates/tsunagi-core/src/task.rs diff --git a/crates/tsunagi/src/testing.rs b/crates/tsunagi-core/src/testing.rs similarity index 100% rename from crates/tsunagi/src/testing.rs rename to crates/tsunagi-core/src/testing.rs diff --git a/crates/tsunagi/tests/authentication.rs b/crates/tsunagi-core/tests/authentication.rs similarity index 100% rename from crates/tsunagi/tests/authentication.rs rename to crates/tsunagi-core/tests/authentication.rs diff --git a/crates/tsunagi/tests/cache_and_state.rs b/crates/tsunagi-core/tests/cache_and_state.rs similarity index 100% rename from crates/tsunagi/tests/cache_and_state.rs rename to crates/tsunagi-core/tests/cache_and_state.rs diff --git a/crates/tsunagi/tests/device_identity.rs b/crates/tsunagi-core/tests/device_identity.rs similarity index 100% rename from crates/tsunagi/tests/device_identity.rs rename to crates/tsunagi-core/tests/device_identity.rs diff --git a/crates/tsunagi/tests/discovery.rs b/crates/tsunagi-core/tests/discovery.rs similarity index 100% rename from crates/tsunagi/tests/discovery.rs rename to crates/tsunagi-core/tests/discovery.rs diff --git a/crates/tsunagi/tests/discovery_lifecycle.rs b/crates/tsunagi-core/tests/discovery_lifecycle.rs similarity index 100% rename from crates/tsunagi/tests/discovery_lifecycle.rs rename to crates/tsunagi-core/tests/discovery_lifecycle.rs diff --git a/crates/tsunagi/tests/end_to_end.rs b/crates/tsunagi-core/tests/end_to_end.rs similarity index 100% rename from crates/tsunagi/tests/end_to_end.rs rename to crates/tsunagi-core/tests/end_to_end.rs diff --git a/crates/tsunagi-core/tests/identity.rs b/crates/tsunagi-core/tests/identity.rs new file mode 100644 index 0000000..d7dba37 --- /dev/null +++ b/crates/tsunagi-core/tests/identity.rs @@ -0,0 +1,173 @@ +//! Scenario 1: deterministic network identity. +//! +//! The same name and secret must yield the same network space on different +//! devices, and nothing else — hostname, device key, restart — may change it. + +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use tsunagi::Agent; +use tsunagi::discovery::SharedMemoryDiscovery; +use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret}; +use tsunagi::testing::{TestAgent, config_with, network}; + +#[test] +fn derivation_is_a_pure_function_of_name_and_secret() { + let name = NetworkName::new("home").unwrap(); + let other = NetworkName::new("Home").unwrap(); + let secret = NetworkSecret::generate(); + let other_secret = NetworkSecret::generate(); + + let a = NetworkKeys::derive(&name, &secret); + let b = NetworkKeys::derive(&name, &secret); + assert_eq!(a.network_id(), b.network_id()); + assert_eq!(a.discovery_key(), b.discovery_key()); + assert_eq!(a.descriptor(), b.descriptor()); + + // A different name is a different space. Names are used verbatim, so case + // matters. + assert_ne!( + a.network_id(), + NetworkKeys::derive(&other, &secret).network_id() + ); + // A different secret is a different space. + assert_ne!( + a.network_id(), + NetworkKeys::derive(&name, &other_secret).network_id() + ); + // Separated key material: the discovery key is not the network id. + assert_ne!(a.network_id().as_bytes(), a.discovery_key().as_bytes()); +} + +#[test] +fn descriptor_carries_no_creator_time_or_secret() { + let name = NetworkName::new("shared").unwrap(); + let secret = NetworkSecret::generate(); + let first = NetworkKeys::derive(&name, &secret).descriptor(); + let second = NetworkKeys::derive(&name, &secret).descriptor(); + + // Two independently built descriptors are byte identical: no random + // creator id, no creation timestamp, no owner signature. + assert_eq!(first.to_canonical_bytes(), second.to_canonical_bytes()); + + let encoded = first.to_canonical_bytes(); + let secret_bytes = secret.encode(); + assert!( + !encoded + .windows(secret_bytes.len()) + .any(|window| window == secret_bytes.as_bytes()), + "the secret must never appear in the public descriptor" + ); +} + +#[test] +fn names_are_validated_not_silently_normalised() { + assert!(NetworkName::new("").is_err()); + assert!(NetworkName::new(" home").is_err(), "must not be trimmed"); + assert!(NetworkName::new("home ").is_err(), "must not be trimmed"); + assert!(NetworkName::new("ho\nme").is_err()); + assert!(NetworkName::new("a".repeat(65)).is_err()); + assert_eq!(NetworkName::new("home").unwrap().as_str(), "home"); +} + +#[test] +fn secrets_are_not_truncated_or_normalised() { + let secret = NetworkSecret::generate(); + let text = secret.encode(); + let round_tripped = NetworkSecret::decode(&text).unwrap(); + assert_eq!(secret, round_tripped); + + // Short secrets are rejected rather than stretched. + assert!(NetworkSecret::from_bytes(vec![7u8; 15]).is_err()); + assert!(NetworkSecret::from_bytes(vec![7u8; 16]).is_ok()); + + // Debug output must not leak the secret. + let rendered = format!("{secret:?}"); + assert_eq!(rendered, "NetworkSecret()"); +} + +#[tokio::test] +async fn different_devices_and_hostnames_agree_on_the_network_id() { + let discovery = SharedMemoryDiscovery::new(); + let (name, secret) = network("agreement"); + + let a = TestAgent::spawn_with(|cfg| cfg.with_hostname("alpha"), &discovery) + .await + .unwrap(); + let b = TestAgent::spawn_with(|cfg| cfg.with_hostname("beta"), &discovery) + .await + .unwrap(); + + assert_ne!( + a.agent.endpoint_id(), + b.agent.endpoint_id(), + "different devices must have different endpoint ids" + ); + assert_eq!(a.agent.hostname(), "alpha"); + assert_eq!(b.agent.hostname(), "beta"); + + let id_a = a.agent.join_network(&name, &secret).await.unwrap(); + let id_b = b.agent.join_network(&name, &secret).await.unwrap(); + assert_eq!(id_a, id_b); + + a.agent.shutdown().await; + b.agent.shutdown().await; +} + +#[tokio::test] +async fn restart_keeps_the_device_and_network_identity() { + let discovery = SharedMemoryDiscovery::new(); + let (name, secret) = network("stable"); + + let first = TestAgent::spawn(&discovery).await.unwrap(); + let device_id = first.agent.endpoint_id(); + let network_id = first.agent.join_network(&name, &secret).await.unwrap(); + let dir = first.stop().await; + + let reopened = Agent::spawn(config_with(dir.path(), &discovery)) + .await + .unwrap(); + assert_eq!( + reopened.endpoint_id(), + device_id, + "restarting must not mint a new peer" + ); + let networks = reopened.list_networks().await.unwrap(); + assert_eq!(networks.len(), 1); + assert_eq!(networks[0].network_id, network_id); + assert!(networks[0].active, "auto-start networks come back up"); + + reopened.shutdown().await; + drop(reopened); + drop(dir); +} + +#[tokio::test] +async fn changing_the_secret_keeps_the_device_identity() { + let discovery = SharedMemoryDiscovery::new(); + let dir = tempfile::TempDir::new().unwrap(); + let agent = Agent::spawn(config_with(dir.path(), &discovery)) + .await + .unwrap(); + let device_id = agent.endpoint_id(); + + let name = NetworkName::new("rotating").unwrap(); + let old = agent + .join_network(&name, &NetworkSecret::generate()) + .await + .unwrap(); + let new = agent + .join_network(&name, &NetworkSecret::generate()) + .await + .unwrap(); + + assert_ne!(old, new, "a new secret is a new network space"); + assert_eq!( + agent.endpoint_id(), + device_id, + "rotating the network secret must not change the persistent iroh id" + ); + + agent.shutdown().await; + drop(agent); + drop(dir); +} diff --git a/crates/tsunagi/tests/leaving.rs b/crates/tsunagi-core/tests/leaving.rs similarity index 100% rename from crates/tsunagi/tests/leaving.rs rename to crates/tsunagi-core/tests/leaving.rs diff --git a/crates/tsunagi/tests/mainline.rs b/crates/tsunagi-core/tests/mainline.rs similarity index 100% rename from crates/tsunagi/tests/mainline.rs rename to crates/tsunagi-core/tests/mainline.rs diff --git a/crates/tsunagi/tests/mainline_socket.rs b/crates/tsunagi-core/tests/mainline_socket.rs similarity index 100% rename from crates/tsunagi/tests/mainline_socket.rs rename to crates/tsunagi-core/tests/mainline_socket.rs diff --git a/crates/tsunagi/tests/multi_peer.rs b/crates/tsunagi-core/tests/multi_peer.rs similarity index 100% rename from crates/tsunagi/tests/multi_peer.rs rename to crates/tsunagi-core/tests/multi_peer.rs diff --git a/crates/tsunagi/tests/network_isolation.rs b/crates/tsunagi-core/tests/network_isolation.rs similarity index 100% rename from crates/tsunagi/tests/network_isolation.rs rename to crates/tsunagi-core/tests/network_isolation.rs diff --git a/crates/tsunagi/tests/resilience.rs b/crates/tsunagi-core/tests/resilience.rs similarity index 100% rename from crates/tsunagi/tests/resilience.rs rename to crates/tsunagi-core/tests/resilience.rs diff --git a/crates/tsunagi/tests/restart.rs b/crates/tsunagi-core/tests/restart.rs similarity index 100% rename from crates/tsunagi/tests/restart.rs rename to crates/tsunagi-core/tests/restart.rs diff --git a/crates/tsunagi-tcp-tls/Cargo.toml b/crates/tsunagi-tcp-tls/Cargo.toml index 34e1a3b..bcc6539 100644 --- a/crates/tsunagi-tcp-tls/Cargo.toml +++ b/crates/tsunagi-tcp-tls/Cargo.toml @@ -8,7 +8,7 @@ license.workspace = true repository.workspace = true [dependencies] -tsunagi = { path = "../tsunagi", version = "0.1.0" } +tsunagi-core = { path = "../tsunagi-core", version = "0.1.0" } iroh.workspace = true tokio = { workspace = true, features = ["net", "io-util", "time", "sync", "macros"] } rustls = "0.23" @@ -33,7 +33,7 @@ netdev = "0.46" caps = "0.5" [dev-dependencies] -tsunagi = { path = "../tsunagi", features = ["testing"] } +tsunagi-core = { path = "../tsunagi-core", features = ["testing"] } tempfile.workspace = true tracing-subscriber.workspace = true diff --git a/crates/tsunagi-tray/Cargo.toml b/crates/tsunagi-tray/Cargo.toml index 56594dd..89f5450 100644 --- a/crates/tsunagi-tray/Cargo.toml +++ b/crates/tsunagi-tray/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" # The agent control client and the report types: the GUI is a thin client over # the same local control interface the CLI uses, so it reuses these directly # and never re-implements the protocol. -tsunagi = { path = "../tsunagi", version = "0.1.0" } +tsunagi-core = { path = "../tsunagi-core", version = "0.1.0" } # Native, lightweight GUI — no web engine. eframe re-exports egui. eframe = { version = "0.31", default-features = false, features = ["default_fonts", "glow", "wayland", "x11"] } # Cross-platform tray icon + menu (Windows tray, macOS menu bar, Linux SNI). diff --git a/crates/tsunagi-wg-quic/Cargo.toml b/crates/tsunagi-wg-quic/Cargo.toml index 51fa0e6..460d268 100644 --- a/crates/tsunagi-wg-quic/Cargo.toml +++ b/crates/tsunagi-wg-quic/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true repository.workspace = true [dependencies] -tsunagi = { path = "../tsunagi", version = "0.1.0" } +tsunagi-core = { path = "../tsunagi-core", version = "0.1.0" } iroh.workspace = true tokio.workspace = true serde.workspace = true @@ -30,7 +30,7 @@ rand = "0.10" boringtun = { version = "0.7.1", default-features = false } [dev-dependencies] -tsunagi = { path = "../tsunagi", features = ["testing"] } +tsunagi-core = { path = "../tsunagi-core", features = ["testing"] } tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros"] } tempfile.workspace = true tracing-subscriber.workspace = true diff --git a/crates/tsunagi-wg-quic/tests/local_control.rs b/crates/tsunagi-wg-quic/tests/local_control.rs index 3bc8d89..7ae72dc 100644 --- a/crates/tsunagi-wg-quic/tests/local_control.rs +++ b/crates/tsunagi-wg-quic/tests/local_control.rs @@ -1,7 +1,7 @@ //! The local control interface: a client asking a running agent for status. //! //! Uses a real Unix socket on a temporary path, the real agent and the real -//! WireGuard data plane, so what a `tsunagi status` client would see is what +//! WireGuard data plane, so what a `tsng status` client would see is what //! is checked here. #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] diff --git a/crates/tsunagi-wireguard/Cargo.toml b/crates/tsunagi-wireguard/Cargo.toml index 2681bf9..65e1418 100644 --- a/crates/tsunagi-wireguard/Cargo.toml +++ b/crates/tsunagi-wireguard/Cargo.toml @@ -8,7 +8,7 @@ license.workspace = true repository.workspace = true [dependencies] -tsunagi = { path = "../tsunagi", version = "0.1.0" } +tsunagi-core = { path = "../tsunagi-core", version = "0.1.0" } iroh.workspace = true tokio = { workspace = true, features = ["net", "io-util", "time", "sync", "macros"] } serde.workspace = true @@ -26,7 +26,7 @@ boringtun = { version = "0.7.1", default-features = false } netdev = "0.46" [dev-dependencies] -tsunagi = { path = "../tsunagi", features = ["testing"] } +tsunagi-core = { path = "../tsunagi-core", features = ["testing"] } tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros"] } tempfile.workspace = true tracing-subscriber.workspace = true diff --git a/crates/tsunagi/Cargo.toml b/crates/tsunagi/Cargo.toml index 1c50875..db16a93 100644 --- a/crates/tsunagi/Cargo.toml +++ b/crates/tsunagi/Cargo.toml @@ -1,110 +1,43 @@ [package] name = "tsunagi" version = "0.1.0" -description = "Small private mesh networks: persistent agent identity, deterministic network spaces, an iroh control plane, signed state and a local DNS view." -readme = "../../README.md" -keywords = ["mesh", "p2p", "iroh", "networking"] -categories = ["network-programming"] +description = "The tsunagi command line agent." edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -[features] -default = [] -# A real TUN device, so a plugin can carry actual IP traffic. On Linux it -# needs CAP_NET_ADMIN; on Windows the process must run elevated and -# `wintun.dll` must be reachable. Without either the in-memory device serves -# tests. The platform-specific dependencies are declared per target, so -# enabling this only pulls what the target it is built for actually uses. -tun-device = [ - "dep:tun", - "dep:rtnetlink", - "dep:caps", - "dep:futures-util", -] -# Telling the operating system where to send its DNS questions. systemd-resolved -# on Linux, the Name Resolution Policy Table on Windows; the zone and the -# server work without it either way. -dns-publish = ["dep:zbus"] -# The test harness in `testing`, for this crate's own tests and for a -# protocol crate's. Not on by default: it is only of use to a test. -testing = ["dep:tempfile", "dep:tracing-subscriber"] +[[bin]] +name = "tsng" +path = "src/main.rs" [dependencies] -arc-swap = "1.9" +tsunagi-core = { path = "../tsunagi-core", version = "0.1.0", features = ["tun-device", "dns-publish"] } +tsunagi-wireguard = { path = "../tsunagi-wireguard", version = "0.1.0" } +tsunagi-wg-quic = { path = "../tsunagi-wg-quic", version = "0.1.0" } +tsunagi-tcp-tls = { path = "../tsunagi-tcp-tls", version = "0.1.0" } iroh.workspace = true -# `net` for the local control interface: a Unix socket on Unix, a named pipe -# on Windows. Both live behind the same `net` feature. -tokio = { workspace = true, features = ["net"] } -tokio-stream = "0.1" -serde.workspace = true -postcard.workspace = true -bytes.workspace = true -thiserror.workspace = true +tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "signal"] } tracing.workspace = true -hex.workspace = true -data-encoding.workspace = true -rusqlite = { version = "0.40", features = ["bundled"] } -hkdf = "0.13" -hmac = "0.13" -sha2 = "0.11" -subtle = "2.6" -zeroize = { version = "1.9", features = ["derive"] } -rand = "0.10" -# Already in the tree through iroh. A packet codec, not a DNS server: the -# zone logic is ours and a full server framework would be a large dependency -# for answering A records from memory. -simple-dns = "0.12" -# The IANA top-level domain list, compiled in: one function, no -# dependencies, no network. Used only to warn that a zone name shadows a -# real public domain, never to refuse one. -tld = "2.40" -fs4 = { version = "1.1", features = ["sync"] } -directories = "6.0" -# The real system hostname, without a libc call of our own: this crate is -# forbidden `unsafe` and will not make one. -gethostname = "1.1" +tracing-subscriber.workspace = true +clap = { version = "4.5", features = ["derive", "env"] } +# Already in the tree through clap. `anstream` strips the escapes when stdout +# is not a terminal and turns on virtual terminal processing on Windows, so +# colour is never written where it would show up as rubbish. +anstream = "1.0" +anstyle = "1.0" +# Local interface addresses, for the diagnostics in `status`. netwatch = "0.19.3" -netdev = "0.46" -mainline = "8.0" -futures-lite = "2.6" -tun = { version = "0.8", features = ["async"], optional = true } -# Only for the `testing` harness, which a protocol crate's tests use too. -tempfile = { version = "3.24", optional = true } -tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } -# Linux-only interface provisioning. `rtnetlink` configures the interface in -# process, so no `ip` invocation is ever needed; `caps` keeps CAP_NET_ADMIN -# out of the effective set except during the moments it is used. -[target.'cfg(target_os = "linux")'.dependencies] -rtnetlink = { version = "0.23", optional = true } -# Pure Rust D-Bus, no libdbus to link against. `tokio` rather than the -# default reactor, because the agent brings its own. -zbus = { version = "5.19", default-features = false, features = ["tokio"], optional = true } -caps = { version = "0.5", optional = true } -futures-util = { version = "0.3", default-features = false, optional = true } - -# Windows-only interface provisioning. The Wintun adapter itself is created -# and driven through the `tun` dependency, whose safe wrapper is the only way -# in — loading `wintun.dll` is an `unsafe` call this crate forbids. `netdev` -# (already in the tree through `netwatch`) reads the interface table through a -# safe wrapper, which is how the provisioner observes an adapter by name -# before deciding what to change. Creating an adapter needs the process to be -# elevated and `wintun.dll` to be reachable at run time. -[target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { version = "0.61", features = [ - "Win32_Security", - "Win32_Security_Authorization", - "Win32_Foundation", -] } +# Resolving a group name to its gid for `up --control-group`, through a safe +# wrapper (no unsafe of our own). Unix only. +[target.'cfg(unix)'.dependencies] +nix = { version = "0.31", features = ["user"] } [dev-dependencies] -# Its own tests use the harness it publishes. -tsunagi = { path = ".", features = ["testing"] } -tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "process", "test-util"] } +tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "process"] } tempfile.workspace = true -tracing-subscriber.workspace = true +simple-dns = "0.12" [lints] workspace = true diff --git a/crates/tsunagi-cli/src/main.rs b/crates/tsunagi/src/main.rs similarity index 97% rename from crates/tsunagi-cli/src/main.rs rename to crates/tsunagi/src/main.rs index 6a397a2..b1b87f9 100644 --- a/crates/tsunagi-cli/src/main.rs +++ b/crates/tsunagi/src/main.rs @@ -1,4 +1,4 @@ -//! The `tsunagi` command line agent. +//! The `tsng` command line agent. //! //! This binary owns everything the library deliberately refuses to do: it //! starts the tokio runtime, installs a logging subscriber and handles @@ -34,7 +34,7 @@ use tsunagi_wireguard::{ /// A small agent for private mesh networks. #[derive(Debug, Parser)] -#[command(name = "tsunagi", version, about, long_about = None)] +#[command(name = "tsng", version, about, long_about = None)] struct Cli { /// Log filter, for example `info` or `tsunagi=debug`. /// @@ -69,7 +69,7 @@ enum Command { Protocols, /// Makes a network, or joins one — the command that does the work. /// - /// The same as `tsunagi network join`, at the top level because it is + /// The same as `tsng network join`, at the top level because it is /// what gets typed: `up` runs the agent, this decides what it is in. Join(JoinArgs), /// Shows the networks this device belongs to, and leaves them. @@ -79,7 +79,7 @@ enum Command { /// Lists devices offering to be an exit node, and sends all this /// device's internet traffic through one. /// - /// The other half of `tsunagi network exit-node`, which is what makes a + /// The other half of `tsng network exit-node`, which is what makes a /// device offer itself. A device uses one exit node at a time. ExitNode(ExitNodeArgs), /// Removes everything this device has stored and starts over. @@ -205,8 +205,8 @@ enum NetworkAction { /// /// The state directory belongs to one live agent, so this is how a /// network is added to it, and it takes effect at once. With no agent - /// running it is configured and starts with the next `tsunagi up`. - /// `tsunagi join` is the same command, spelled shorter. + /// running it is configured and starts with the next `tsng up`. + /// `tsng join` is the same command, spelled shorter. Join(JoinArgs), /// Stops serving a network, keeping everything so it can be resumed. /// @@ -214,7 +214,7 @@ enum NetworkAction { /// signed state all stay. Sessions close and the address comes off the /// interface, and nothing is announced — to the others this device is /// simply away, as if it had been switched off. It stays stopped - /// across restarts until `tsunagi network start`. + /// across restarts until `tsng network start`. Stop { /// Exact network name, full id or unique id prefix. network: String, @@ -471,7 +471,7 @@ struct UpArgs { /// A protocol setting, as `key=value` or `protocol:key=value`. /// - /// Repeat for several. `tsunagi protocols` lists what each one takes. + /// Repeat for several. `tsng protocols` lists what each one takes. #[arg( short = 'o', long = "protocol-option", @@ -498,13 +498,13 @@ struct UpArgs { /// addresses; questions are taken on loopback of both families, over /// UDP and TCP. /// - /// Overrides a remembered opt-out. Use --no-dns or `tsunagi dns off` to disable. + /// Overrides a remembered opt-out. Use --no-dns or `tsng dns off` to disable. #[arg(long, conflicts_with = "no_dns", help_heading = "System")] dns: bool, /// Disable local DNS and remember this choice for future starts. /// - /// Use --dns or `tsunagi dns on` to enable it again. + /// Use --dns or `tsng dns on` to enable it again. #[arg(long, conflicts_with = "dns", help_heading = "System")] no_dns: bool, @@ -715,7 +715,7 @@ fn resolve_secret( [one] => Ok((one.secret.clone(), SecretOrigin::Stored)), several => Err(format!( "this device is in {} networks called `{name}`, so the name alone does not say \ - which. Give --secret, or `tsunagi network` lists them with their ids.", + which. Give --secret, or `tsng network` lists them with their ids.", several.len() ) .into()), @@ -841,7 +841,7 @@ fn control_socket( return tsunagi::ipc::default_control_socket_path(); } // When the user did not choose a state directory, prefer a running system - // service's socket, so `sudo tsunagi status` reaches the packaged daemon — + // service's socket, so `sudo tsng status` reaches the packaged daemon — // which binds there via TSUNAGI_CONTROL_SOCKET — with no flags. Only when it // is actually present, so a per-user agent still uses its own socket. #[cfg(all(unix, not(target_os = "macos")))] @@ -979,7 +979,7 @@ fn device_section(paths: &StoragePaths, observed: &Observed) -> report::Section /// The networks this device belongs to, named but not described. /// /// No secrets: this is part of `status`, and a status report is somewhere a -/// secret must never appear. `tsunagi network secret` is the place that shows one, +/// secret must never appear. `tsng network secret` is the place that shows one, /// because asking for it there is deliberate. fn configured_networks_section(paths: &StoragePaths) -> report::Section { use report::{Health, Row, Section}; @@ -1299,7 +1299,7 @@ fn update(state: &Arc>, edit: impl FnOnce(&mut DnsSta /// What a protocol is called, what it speaks, and what it takes. /// /// A registry rather than a lookup on the plugins themselves, because -/// `tsunagi protocols` has to answer before anything is constructed, and +/// `tsng protocols` has to answer before anything is constructed, and /// because this is the list `--protocol` resolves against. struct ProtocolSpec { /// The name on the wire, which is what peers compare. @@ -1411,7 +1411,7 @@ fn show_protocols() -> Result<(), Box> { } out.push(section); } - print_report("tsunagi protocols", &out) + print_report("tsng protocols", &out) } /// Serves the local control socket from the running agent. @@ -1734,7 +1734,7 @@ fn resolve_network<'a>( ) -> Result<&'a tsunagi::storage::StoredNetwork, String> { let wanted = wanted.trim_matches(|c: char| c.is_ascii_whitespace()); if wanted.is_empty() { - return Err("select a network by name or id; `tsunagi network` lists them".to_string()); + return Err("select a network by name or id; `tsng network` lists them".to_string()); } if let Some(network) = networks .iter() @@ -1755,7 +1755,7 @@ fn resolve_network<'a>( [one] => Ok(one), [] => Err(format!( "no configured network matches name or id `{wanted}`; \ - `tsunagi network` lists them" + `tsng network` lists them" )), several => { let candidates = several @@ -1771,7 +1771,7 @@ fn resolve_network<'a>( } } -/// `tsunagi network`: what this device belongs to, and leaving it. +/// `tsng network`: what this device belongs to, and leaving it. async fn network_command(args: NetworkArgs) -> Result<(), Box> { let paths = args.paths.resolve()?; let is_custom_state = args.paths.state_dir.is_some(); @@ -1810,7 +1810,7 @@ async fn network_command(args: NetworkArgs) -> Result<(), Box Result<(), Box> { let paths = args.paths.resolve()?; let is_custom_state = args.paths.state_dir.is_some(); @@ -1883,7 +1883,7 @@ async fn join_network( eprintln!( "\nwarning: `{}` is also configured with a different secret, as {}.\n\ A network is its name *and* its secret, so these two share nothing.\n\ - If that was a mistyped secret, `tsunagi network leave` removes one.", + If that was a mistyped secret, `tsng network leave` removes one.", report.name, short(other, 10) ); @@ -1935,7 +1935,7 @@ async fn join_network( if origin == SecretOrigin::Generated { println!(" secret {}", secret.encode().as_str()); } - eprintln!("\nNo agent is running here, so it starts with the next `tsunagi up`."); + eprintln!("\nNo agent is running here, so it starts with the next `tsng up`."); Ok(()) } @@ -1955,15 +1955,15 @@ async fn invite(socket: &std::path::Path, name: &NetworkName, secret: &NetworkSe println!(" secret {}", secret.encode().as_str()); println!( "\nRun this on the other machine:\n\n \ - tsunagi join --network {name} --secret {}", + tsng join --network {name} --secret {}", secret.encode().as_str() ); match endpoint { Some(endpoint) => println!( - "\nStart its agent with `tsunagi up`; DHT discovery is enabled by default.\n\n \ - Optional manual bootstrap: tsunagi up --peer {endpoint}" + "\nStart its agent with `tsng up`; DHT discovery is enabled by default.\n\n \ + Optional manual bootstrap: tsng up --peer {endpoint}" ), - None => println!("\nIts agent has to be running: `tsunagi up`."), + None => println!("\nIts agent has to be running: `tsng up`."), } } @@ -1999,28 +1999,28 @@ async fn show_networks( None => "running · no address agreed yet".to_string(), }, format!( - "`tsunagi network stop {}` pauses it, `leave` gives it up", + "`tsng network stop {}` pauses it, `leave` gives it up", short(&id, 10) ), ), Some(_) => ( "stopped".to_string(), format!( - "kept as it was; `tsunagi network start {}` resumes it", + "kept as it was; `tsng network start {}` resumes it", short(&id, 10) ), ), None if network.auto_start => ( "configured · starts with the agent".to_string(), format!( - "`tsunagi network stop {}` keeps it from starting", + "`tsng network stop {}` keeps it from starting", short(&id, 10) ), ), None => ( "stopped".to_string(), format!( - "kept as it was; `tsunagi network start {}` resumes it", + "kept as it was; `tsng network start {}` resumes it", short(&id, 10) ), ), @@ -2049,7 +2049,7 @@ async fn show_networks( )); } out.push(section); - print_report("tsunagi networks", &out) + print_report("tsng networks", &out) } /// Stops serving a network, or starts serving it again. @@ -2114,7 +2114,7 @@ async fn exit_offer_command( storage.set_exit_node(network.network_id, enabled).await?; storage.release_ownership_lock(); println!( - "exit node {} for `{}` ({}) with the next `tsunagi up`", + "exit node {} for `{}` ({}) with the next `tsng up`", if enabled { "will be offered" } else { @@ -2153,7 +2153,7 @@ async fn exit_offer_command( } println!( "\nMembers can now send all their internet traffic through this device, and it \ - can see it. `tsunagi network exit-node {wanted} off` stops." + can see it. `tsng network exit-node {wanted} off` stops." ); Ok(()) } @@ -2235,12 +2235,12 @@ fn resolve_exit_node<'a>( [] => match named.first() { Some((network, peer)) => Err(format!( "`{}` is connected but does not offer to be an exit node. On that device: \ - tsunagi network exit-node {} on", + tsng network exit-node {} on", peer_label(peer), network.name )), None => Err(format!( - "no connected device matches `{wanted}`. `tsunagi exit-node` lists the \ + "no connected device matches `{wanted}`. `tsng exit-node` lists the \ ones that offer to be an exit node" )), }, @@ -2262,7 +2262,7 @@ fn resolve_exit_node<'a>( } } -/// `tsunagi exit-node`: list, use or stop using an exit node. +/// `tsng exit-node`: list, use or stop using an exit node. async fn exit_node_command(args: ExitNodeArgs) -> Result<(), Box> { use report::{Health, Report, Row, Section}; @@ -2272,7 +2272,7 @@ async fn exit_node_command(args: ExitNodeArgs) -> Result<(), Box Result<(), Box on"), + .with_note("on that device: tsng network exit-node on"), ); } let mut out = Report::new(); out.push(section); - print_report("tsunagi exit-node", &out)?; + print_report("tsng exit-node", &out)?; if any { - println!( - "\n`tsunagi exit-node ` uses one; `tsunagi exit-node off` stops." - ); + println!("\n`tsng exit-node ` uses one; `tsng exit-node off` stops."); } Ok(()) } @@ -2364,7 +2362,7 @@ async fn exit_node_command(args: ExitNodeArgs) -> Result<(), Box Result<(), Box> { let paths = args.paths.resolve()?; let is_custom_state = args.paths.state_dir.is_some(); @@ -2532,7 +2530,7 @@ async fn wipe(args: WipeArgs) -> Result<(), Box> { let networks = stored_networks(&paths); if !args.yes { - println!("`tsunagi wipe --yes` would remove:\n"); + println!("`tsng wipe --yes` would remove:\n"); for entry in plan.entries() { println!(" {}", entry.display()); } @@ -2543,7 +2541,7 @@ async fn wipe(args: WipeArgs) -> Result<(), Box> { } println!( "\nNobody is told. Leave each network first — start the agent and run\n\ - `tsunagi network leave ` — to free the address and name it holds\n\ + `tsng network leave ` — to free the address and name it holds\n\ for the others. Afterwards this device is a stranger: a new identity,\n\ no networks, and no way to sign anything for the old ones." ); @@ -2572,7 +2570,7 @@ async fn wipe(args: WipeArgs) -> Result<(), Box> { Ok(()) } -/// `tsunagi dns`: the local resolver, and turning it on or off. +/// `tsng dns`: the local resolver, and turning it on or off. async fn dns_command(args: DnsArgs) -> Result<(), Box> { let paths = args.paths.resolve()?; let is_custom_state = args.paths.state_dir.is_some(); @@ -2628,7 +2626,7 @@ async fn dns_command(args: DnsArgs) -> Result<(), Box> { }, )?; println!("{} for future starts", if enable { "on" } else { "off" }); - eprintln!("\nNo agent is running here, so it takes effect with the next `tsunagi up`."); + eprintln!("\nNo agent is running here, so it takes effect with the next `tsng up`."); Ok(()) } @@ -2663,10 +2661,10 @@ async fn show_dns( out.push(section); } } - print_report("tsunagi dns", &out) + print_report("tsng dns", &out) } -/// `tsunagi id`: what this device is, and what changes it. +/// `tsng id`: what this device is, and what changes it. async fn id(args: IdArgs) -> Result<(), Box> { let paths = args.paths.resolve()?; let is_custom_state = args.paths.state_dir.is_some(); @@ -2703,13 +2701,13 @@ async fn show_identity( out.push(device); // What this device *is*, not what it belongs to. The networks are - // `tsunagi network`, and their secrets are asked for by name there: + // `tsng network`, and their secrets are asked for by name there: // printing them in an overview put them in every pasted report. let networks = stored_networks(paths); let mut section = Section::new("networks"); section.push(match networks.len() { 0 => Row::new(Health::Info, "none", "no network has been joined") - .with_note("`tsunagi join --network ` makes or joins one"), + .with_note("`tsng join --network ` makes or joins one"), count => Row::new( Health::Info, "joined", @@ -2722,11 +2720,11 @@ async fn show_identity( .join(", ") ), ) - .with_note("`tsunagi network` lists them with their ids and addresses"), + .with_note("`tsng network` lists them with their ids and addresses"), }); out.push(section); - print_report("tsunagi id", &out) + print_report("tsng id", &out) } /// The name this device answers to. @@ -2938,7 +2936,7 @@ async fn status(args: StatusArgs) -> Result<(), Box> { out.push(host_section(&observed)); out.push(addresses_section().await); - print_report("tsunagi status", &out) + print_report("tsng status", &out) } /// The local resolver: whether it answers, and whether the system asks it. @@ -3017,7 +3015,7 @@ fn dns_absent_section() -> report::Section { "no local resolver for any network", ) .with_note( - "members resolve by address only. `tsunagi up --dns` serves \ + "members resolve by address only. `tsng up --dns` serves \ `.` from signed state, so a member that is switched \ off still resolves.", ), @@ -3160,7 +3158,7 @@ fn network_section( ) .with_note( "a network is its name *and* its secret, so these two share nothing. \ - Usually a mistyped secret; `tsunagi network secret` shows which is which.", + Usually a mistyped secret; `tsng network secret` shows which is which.", ), ); } @@ -3377,7 +3375,7 @@ fn exit_rows(network: &tsunagi::ipc::NetworkReport) -> Vec { let row = if !exit.via_online { Row::new(Health::Broken, "using exit", format!("{name} · OFFLINE")).with_note( "internet traffic is dropped until it returns or you run \ - `tsunagi exit-node off`", + `tsng exit-node off`", ) } else { match &exit.client_rules { @@ -3535,7 +3533,7 @@ fn host_section(observed: &Observed) -> report::Section { .collect(); host.push( Row::new(Health::Info, "protocols", protocols.join(", ")) - .with_note("run `tsunagi protocols` to see what each one takes"), + .with_note("run `tsng protocols` to see what each one takes"), ); host.push(Row::new( Health::Info, @@ -3727,7 +3725,7 @@ fn print_report(title: &str, out: &report::Report) -> Result<(), Box bool { @@ -4075,7 +4073,7 @@ mod report { #[test] fn a_report_of_plain_facts_claims_nothing_at_the_end() { - // `tsunagi id` reports identifiers, not checks. Summarising them + // `tsng id` reports identifiers, not checks. Summarising them // as fine would assert something that was never tested. let mut report = Report::new(); let mut section = Section::new("device"); @@ -4213,7 +4211,7 @@ async fn up(args: UpArgs) -> Result<(), Box> { let known: Vec<&str> = PROTOCOLS.iter().map(|spec| spec.name).collect(); return Err(format!( "this build has no protocol called `{name}`; it has {}. \ - Run `tsunagi protocols` to see what each one takes.", + Run `tsng protocols` to see what each one takes.", known.join(", ") ) .into()); @@ -4232,7 +4230,7 @@ async fn up(args: UpArgs) -> Result<(), Box> { .any(|spec| !settings_for(spec, std::slice::from_ref(setting)).is_empty()) { return Err(format!( - "no selected protocol takes `{}`; run `tsunagi protocols` to see what they do", + "no selected protocol takes `{}`; run `tsng protocols` to see what they do", setting.key ) .into()); @@ -4343,13 +4341,13 @@ async fn up(args: UpArgs) -> Result<(), Box> { "an agent is already running for {}, and one state directory is one \ agent — it is the device, not a network.\n\n\ To add a network to it:\n\n \ - tsunagi join --network \n\n\ + tsng join --network \n\n\ To run a second, separate agent instead, give it everything of its \ own:\n\n \ - tsunagi up --state-dir --cache-dir --interface tsun1 \ + tsng up --state-dir --cache-dir --interface tsun1 \ --ipv4-range \n\n\ That is a different identity with its own interface, not this one \ - with another network. `tsunagi network` lists what this one has.", + with another network. `tsng network` lists what this one has.", path.display() ) .into()); @@ -4403,14 +4401,14 @@ async fn up(args: UpArgs) -> Result<(), Box> { protocol_names.join(", ") ); // What this device belongs to is a separate question from whether its - // agent is running, and `tsunagi join` answers it at any time. + // agent is running, and `tsng join` answers it at any time. let configured = agent.list_networks().await.unwrap_or_default(); let running = configured.iter().filter(|network| network.active).count(); println!( " networks {}", match configured.len() { - 0 => "none yet · `tsunagi join --network ` makes or joins one".to_string(), - total => format!("{running} of {total} running · `tsunagi network` lists them"), + 0 => "none yet · `tsng join --network ` makes or joins one".to_string(), + total => format!("{running} of {total} running · `tsng network` lists them"), } ); println!(" state {}", paths.state_dir.display()); @@ -4424,7 +4422,7 @@ async fn up(args: UpArgs) -> Result<(), Box> { None })); - // Serve `tsunagi status` for as long as this agent runs. Failing to bind + // Serve `tsng status` for as long as this agent runs. Failing to bind // is not fatal: the agent itself works fine without it. let control = { let agent = agent.clone(); @@ -4458,7 +4456,7 @@ async fn up(args: UpArgs) -> Result<(), Box> { Some(socket) } Err(err) => { - eprintln!("warning: `tsunagi status` will not work: {err}"); + eprintln!("warning: `tsng status` will not work: {err}"); None } } @@ -5476,7 +5474,7 @@ mod status_tests { assert_eq!(out.worst(), Health::Broken, "{text}"); assert!(text.contains("OFFLINE"), "{text}"); assert!(text.contains("dropped until it returns"), "{text}"); - assert!(text.contains("tsunagi exit-node off"), "{text}"); + assert!(text.contains("tsng exit-node off"), "{text}"); } #[test] @@ -5630,14 +5628,13 @@ mod network_tests { (vec!["--reach", "local", "--dht"], false), (vec!["--reach", "direct"], true), ] { - let cli = - super::Cli::try_parse_from(["tsunagi", "up"].into_iter().chain(flags)).unwrap(); + let cli = super::Cli::try_parse_from(["tsng", "up"].into_iter().chain(flags)).unwrap(); let super::Command::Up(args) = cli.command else { panic!("expected up"); }; assert_eq!(super::dht_enabled(&args), expected); } - assert!(super::Cli::try_parse_from(["tsunagi", "up", "--dht", "--no-dht"]).is_err()); + assert!(super::Cli::try_parse_from(["tsng", "up", "--dht", "--no-dht"]).is_err()); } use super::*; @@ -5814,7 +5811,7 @@ mod secret_tests { #[test] fn a_name_this_device_already_has_resumes_it() { - // Otherwise `tsunagi join --network lab` would invent a stranger with + // Otherwise `tsng join --network lab` would invent a stranger with // the same name every time, which is the confusion this whole // report format exists to prevent. let dir = tempfile::tempdir().unwrap(); diff --git a/crates/tsunagi-cli/tests/dns_service.rs b/crates/tsunagi/tests/dns_service.rs similarity index 99% rename from crates/tsunagi-cli/tests/dns_service.rs rename to crates/tsunagi/tests/dns_service.rs index 82b1c2e..f3fcbd6 100644 --- a/crates/tsunagi-cli/tests/dns_service.rs +++ b/crates/tsunagi/tests/dns_service.rs @@ -103,7 +103,7 @@ impl Running { /// Runs another `tsunagi` command against this agent's directory. fn run(&self, args: &[&str]) -> std::process::Output { - std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi")) + std::process::Command::new(env!("CARGO_BIN_EXE_tsng")) .args(args) .arg("--state-dir") .arg(self.dir.path().join("state")) @@ -174,7 +174,7 @@ fn spawn_child( dns_port: Option, dns_flag: Option<&str>, ) -> std::process::Child { - let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi")); + let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_tsng")); command .arg("up") .arg("--state-dir") @@ -338,7 +338,7 @@ fn explicit_dns_choices_override_the_saved_setting_and_survive_restart() { #[test] fn opposing_dns_flags_are_rejected() { - let result = std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi")) + let result = std::process::Command::new(env!("CARGO_BIN_EXE_tsng")) .args(["up", "--dns", "--no-dns"]) .output() .unwrap(); diff --git a/crates/tsunagi/tests/identity.rs b/crates/tsunagi/tests/identity.rs index d7dba37..e57f05b 100644 --- a/crates/tsunagi/tests/identity.rs +++ b/crates/tsunagi/tests/identity.rs @@ -1,173 +1,56 @@ -//! Scenario 1: deterministic network identity. +//! What the command line says about this device, against a running agent. //! -//! The same name and secret must yield the same network space on different -//! devices, and nothing else — hostname, device key, restart — may change it. +//! These drive the real binary, which is why they live beside it rather +//! than with the library's own tests: the library cannot depend on a binary +//! built from a crate that depends on it. #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +use tempfile::TempDir; use tsunagi::Agent; -use tsunagi::discovery::SharedMemoryDiscovery; -use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret}; -use tsunagi::testing::{TestAgent, config_with, network}; +use tsunagi::config::{AgentConfig, StoragePaths, TransportPolicy}; -#[test] -fn derivation_is_a_pure_function_of_name_and_secret() { - let name = NetworkName::new("home").unwrap(); - let other = NetworkName::new("Home").unwrap(); - let secret = NetworkSecret::generate(); - let other_secret = NetworkSecret::generate(); +/// Asking who this device is must not need the directory lock. +/// +/// The lock belongs to the one agent allowed to *write* the state. `id` only +/// reads, so making it take the lock would mean the question could never be +/// answered while an agent was running — which is exactly when you want to +/// ask it. +#[tokio::test] +async fn identity_can_be_read_while_an_agent_holds_the_directory() { + let dir = TempDir::new().unwrap(); - let a = NetworkKeys::derive(&name, &secret); - let b = NetworkKeys::derive(&name, &secret); - assert_eq!(a.network_id(), b.network_id()); - assert_eq!(a.discovery_key(), b.discovery_key()); - assert_eq!(a.descriptor(), b.descriptor()); + // Hold the directory the way a running agent does. + let agent = Agent::spawn( + AgentConfig::new(StoragePaths::under(dir.path())) + .with_transport(TransportPolicy::LocalOnly) + .with_loopback_bind(), + ) + .await + .unwrap(); + let expected = agent.endpoint_id().to_string(); - // A different name is a different space. Names are used verbatim, so case - // matters. - assert_ne!( - a.network_id(), - NetworkKeys::derive(&other, &secret).network_id() - ); - // A different secret is a different space. - assert_ne!( - a.network_id(), - NetworkKeys::derive(&name, &other_secret).network_id() - ); - // Separated key material: the discovery key is not the network id. - assert_ne!(a.network_id().as_bytes(), a.discovery_key().as_bytes()); -} + let output = tokio::process::Command::new(env!("CARGO_BIN_EXE_tsng")) + .arg("id") + // The same layout `StoragePaths::under` gives the agent above. + .arg("--state-dir") + .arg(dir.path().join("state")) + .arg("--cache-dir") + .arg(dir.path().join("cache")) + .output() + .await + .unwrap(); -#[test] -fn descriptor_carries_no_creator_time_or_secret() { - let name = NetworkName::new("shared").unwrap(); - let secret = NetworkSecret::generate(); - let first = NetworkKeys::derive(&name, &secret).descriptor(); - let second = NetworkKeys::derive(&name, &secret).descriptor(); - - // Two independently built descriptors are byte identical: no random - // creator id, no creation timestamp, no owner signature. - assert_eq!(first.to_canonical_bytes(), second.to_canonical_bytes()); - - let encoded = first.to_canonical_bytes(); - let secret_bytes = secret.encode(); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); assert!( - !encoded - .windows(secret_bytes.len()) - .any(|window| window == secret_bytes.as_bytes()), - "the secret must never appear in the public descriptor" + output.status.success(), + "`id` failed while an agent was running:\n{stderr}" ); -} - -#[test] -fn names_are_validated_not_silently_normalised() { - assert!(NetworkName::new("").is_err()); - assert!(NetworkName::new(" home").is_err(), "must not be trimmed"); - assert!(NetworkName::new("home ").is_err(), "must not be trimmed"); - assert!(NetworkName::new("ho\nme").is_err()); - assert!(NetworkName::new("a".repeat(65)).is_err()); - assert_eq!(NetworkName::new("home").unwrap().as_str(), "home"); -} - -#[test] -fn secrets_are_not_truncated_or_normalised() { - let secret = NetworkSecret::generate(); - let text = secret.encode(); - let round_tripped = NetworkSecret::decode(&text).unwrap(); - assert_eq!(secret, round_tripped); - - // Short secrets are rejected rather than stretched. - assert!(NetworkSecret::from_bytes(vec![7u8; 15]).is_err()); - assert!(NetworkSecret::from_bytes(vec![7u8; 16]).is_ok()); - - // Debug output must not leak the secret. - let rendered = format!("{secret:?}"); - assert_eq!(rendered, "NetworkSecret()"); -} - -#[tokio::test] -async fn different_devices_and_hostnames_agree_on_the_network_id() { - let discovery = SharedMemoryDiscovery::new(); - let (name, secret) = network("agreement"); - - let a = TestAgent::spawn_with(|cfg| cfg.with_hostname("alpha"), &discovery) - .await - .unwrap(); - let b = TestAgent::spawn_with(|cfg| cfg.with_hostname("beta"), &discovery) - .await - .unwrap(); - - assert_ne!( - a.agent.endpoint_id(), - b.agent.endpoint_id(), - "different devices must have different endpoint ids" - ); - assert_eq!(a.agent.hostname(), "alpha"); - assert_eq!(b.agent.hostname(), "beta"); - - let id_a = a.agent.join_network(&name, &secret).await.unwrap(); - let id_b = b.agent.join_network(&name, &secret).await.unwrap(); - assert_eq!(id_a, id_b); - - a.agent.shutdown().await; - b.agent.shutdown().await; -} - -#[tokio::test] -async fn restart_keeps_the_device_and_network_identity() { - let discovery = SharedMemoryDiscovery::new(); - let (name, secret) = network("stable"); - - let first = TestAgent::spawn(&discovery).await.unwrap(); - let device_id = first.agent.endpoint_id(); - let network_id = first.agent.join_network(&name, &secret).await.unwrap(); - let dir = first.stop().await; - - let reopened = Agent::spawn(config_with(dir.path(), &discovery)) - .await - .unwrap(); - assert_eq!( - reopened.endpoint_id(), - device_id, - "restarting must not mint a new peer" - ); - let networks = reopened.list_networks().await.unwrap(); - assert_eq!(networks.len(), 1); - assert_eq!(networks[0].network_id, network_id); - assert!(networks[0].active, "auto-start networks come back up"); - - reopened.shutdown().await; - drop(reopened); - drop(dir); -} - -#[tokio::test] -async fn changing_the_secret_keeps_the_device_identity() { - let discovery = SharedMemoryDiscovery::new(); - let dir = tempfile::TempDir::new().unwrap(); - let agent = Agent::spawn(config_with(dir.path(), &discovery)) - .await - .unwrap(); - let device_id = agent.endpoint_id(); - - let name = NetworkName::new("rotating").unwrap(); - let old = agent - .join_network(&name, &NetworkSecret::generate()) - .await - .unwrap(); - let new = agent - .join_network(&name, &NetworkSecret::generate()) - .await - .unwrap(); - - assert_ne!(old, new, "a new secret is a new network space"); - assert_eq!( - agent.endpoint_id(), - device_id, - "rotating the network secret must not change the persistent iroh id" + assert!( + stdout.contains(&expected), + "expected {expected} in:\n{stdout}" ); agent.shutdown().await; - drop(agent); - drop(dir); } diff --git a/crates/tsunagi-cli/tests/network_cli.rs b/crates/tsunagi/tests/network_cli.rs similarity index 99% rename from crates/tsunagi-cli/tests/network_cli.rs rename to crates/tsunagi/tests/network_cli.rs index 1d48468..53f84bf 100644 --- a/crates/tsunagi-cli/tests/network_cli.rs +++ b/crates/tsunagi/tests/network_cli.rs @@ -30,7 +30,7 @@ impl Running { } fn run_in(dir: &TempDir, args: &[&str]) -> std::process::Output { - std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi")) + std::process::Command::new(env!("CARGO_BIN_EXE_tsng")) .args(args) .arg("--state-dir") .arg(dir.path().join("state")) @@ -51,7 +51,7 @@ fn read_networks(dir: &TempDir) -> Vec { /// anything has been decided. fn start_bare(port: u16) -> Running { let dir = TempDir::new().unwrap(); - let child = std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi")) + let child = std::process::Command::new(env!("CARGO_BIN_EXE_tsng")) .arg("up") .arg("--state-dir") .arg(dir.path().join("state")) @@ -111,7 +111,7 @@ fn joining_with_no_secret_makes_one_and_prints_what_to_send() { assert!(secret.starts_with("tsn1"), "{out}"); let command = out .lines() - .find(|line| line.contains("tsunagi join --network spontaneous")) + .find(|line| line.contains("tsng join --network spontaneous")) .expect("a command to send"); assert!(command.contains(secret), "with the secret in it: {out}"); // And where to find this device, for an agent that is not up yet. @@ -244,7 +244,7 @@ fn up_is_the_agent_and_takes_no_network_at_all() { // second way to do the same thing, and the one that cannot be undone // without a restart. for argument in ["--network", "--secret"] { - let out = std::process::Command::new(env!("CARGO_BIN_EXE_tsunagi")) + let out = std::process::Command::new(env!("CARGO_BIN_EXE_tsng")) .args(["up", argument, "whatever"]) .arg("--state-dir") .arg(TempDir::new().unwrap().path().join("state")) diff --git a/dist/arch/README.md b/dist/arch/README.md index c0a22c6..2e19d7e 100644 --- a/dist/arch/README.md +++ b/dist/arch/README.md @@ -6,7 +6,7 @@ Install the package built for your architecture: sudo pacman -U tsunagi-*-x86_64.pkg.tar.zst ``` -It drops a static, dependency-free binary at `/usr/bin/tsunagi`, a systemd +It drops a static, dependency-free binary at `/usr/bin/tsng`, a systemd service, and a polkit rule, and creates a dedicated system user `tsunagi` the service runs as. @@ -21,8 +21,8 @@ user; `sudo` reaches it with no extra flags, because the CLI finds the running service's socket at `/run/tsunagi/agent.sock`): ```sh -sudo tsunagi status -sudo tsunagi join +sudo tsng status +sudo tsng join ``` ## How it runs @@ -39,7 +39,7 @@ creates and owns its directories: Installing the package enables and starts the service, and adds the user who ran pacman to the `tsunagi` group (log out and in again for it to apply). The -control socket is open to that group, so members can run `tsunagi status`, the +control socket is open to that group, so members can run `tsng status`, the CLI and the tray GUI without sudo; anyone else adds themselves with `sudo usermod -aG tsunagi "$USER"`. The group also lets its members configure systemd-resolved for the overlay zones through the polkit rule. diff --git a/dist/arch/build-pkg.sh b/dist/arch/build-pkg.sh index a5572b2..d4aeb9c 100755 --- a/dist/arch/build-pkg.sh +++ b/dist/arch/build-pkg.sh @@ -31,10 +31,10 @@ pkgrel=1 # Payload. Arch keeps units under /usr/lib/systemd/system and installs to # /usr/bin, so the service's ExecStart is rewritten to match. -install -D -m 0755 "$binary" "$root/usr/bin/tsunagi" +install -D -m 0755 "$binary" "$root/usr/bin/tsng" mkdir -p "$root/usr/lib/systemd/system" -sed 's#/usr/local/bin/tsunagi#/usr/bin/tsunagi#' \ +sed 's#/usr/local/bin/tsng#/usr/bin/tsng#' \ "$here/../linux/tsunagi.service" > "$root/usr/lib/systemd/system/tsunagi.service" chmod 0644 "$root/usr/lib/systemd/system/tsunagi.service" diff --git a/dist/deb/build-deb.sh b/dist/deb/build-deb.sh index cdc2572..a8e5eed 100755 --- a/dist/deb/build-deb.sh +++ b/dist/deb/build-deb.sh @@ -25,10 +25,10 @@ trap 'rm -rf "$root"' EXIT # Payload. Debian installs to /usr/bin (not /usr/local), so the service's # ExecStart is rewritten to match; the source unit targets /usr/local/bin for # the tarball installer. -install -D -m 0755 "$binary" "$root/usr/bin/tsunagi" +install -D -m 0755 "$binary" "$root/usr/bin/tsng" mkdir -p "$root/lib/systemd/system" -sed 's#/usr/local/bin/tsunagi#/usr/bin/tsunagi#' \ +sed 's#/usr/local/bin/tsng#/usr/bin/tsng#' \ "$here/../linux/tsunagi.service" > "$root/lib/systemd/system/tsunagi.service" chmod 0644 "$root/lib/systemd/system/tsunagi.service" @@ -43,7 +43,7 @@ desc_gui="" if [ -n "$tray" ]; then pkgname=tsunagi-gui # The GUI package is a superset of the headless one and must not coexist - # with it (both ship /usr/bin/tsunagi). + # with it (both ship /usr/bin/tsng). extra_control=$'Conflicts: tsunagi\nReplaces: tsunagi\nProvides: tsunagi\n' # What the tray and its window load at run time. extra_control+=$'Depends: libgtk-3-0 | libgtk-3-0t64, libayatana-appindicator3-1 | libappindicator3-1, libxdo3, libxkbcommon0, libwayland-client0, libx11-6, libgl1\n' diff --git a/dist/linux/README.md b/dist/linux/README.md index 8510fac..9e45bab 100644 --- a/dist/linux/README.md +++ b/dist/linux/README.md @@ -27,27 +27,27 @@ group, so after `sudo usermod -aG tsunagi "$USER"` (and logging in again) no `sudo` is needed: ```sh -sudo tsunagi status -sudo tsunagi join +sudo tsng status +sudo tsng join ``` ## As your own user — one capability, no service ```sh -sudo install -m 0755 tsunagi /usr/local/bin/tsunagi -sudo setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsunagi +sudo install -m 0755 tsunagi /usr/local/bin/tsng +sudo setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsng sudo usermod -aG tsunagi "$USER" # so the resolver works; log in again after -tsunagi up +tsng up ``` Nothing runs as root. The capability is lost on every rebuild or copy of the binary, so re-run `setcap` after replacing it. The agent uses your per-user -state directory and socket, so plain `tsunagi status` (no sudo) reaches it. +state directory and socket, so plain `tsng status` (no sudo) reaches it. ## Without touching the OS ```sh -tsunagi up --no-tun +tsng up --no-tun ``` Tunnels form and handshake between agents, but no interface, address or route is diff --git a/dist/linux/install.sh b/dist/linux/install.sh index f60a8c9..e777909 100755 --- a/dist/linux/install.sh +++ b/dist/linux/install.sh @@ -18,7 +18,7 @@ fi echo "Installing tsunagi to ${PREFIX}/bin" install -d "${PREFIX}/bin" -install -m 0755 "${here}/tsunagi" "${PREFIX}/bin/tsunagi" +install -m 0755 "${here}/tsng" "${PREFIX}/bin/tsng" # The dedicated system user the service runs as (works with either shadow's # useradd or Debian's adduser). @@ -47,19 +47,19 @@ Done. Start the agent with: Control it with sudo (the CLI finds the service's socket automatically): - sudo tsunagi status - sudo tsunagi join + sudo tsng status + sudo tsng join EOF else cat < +tsng status +tsng join ``` The agent uses fixed system paths, so the commands find it regardless of whose @@ -34,7 +34,7 @@ For an always-on agent that starts at boot, install the bundled ```sh # Put the binary where the plist expects it (edit the plist for another path). -sudo install -m 0755 target/release/tsunagi /usr/local/bin/tsunagi +sudo install -m 0755 target/release/tsng /usr/local/bin/tsng # Install the daemon. launchd refuses a daemon plist that is not owned by # root:wheel or that is group/world-writable. @@ -56,7 +56,7 @@ tail -f /var/log/tsunagi.log # logs ``` The control commands (`status`, `join`, `network`, `dns`) work the same against -the daemon as against a `sudo tsunagi up`, over the same socket. +the daemon as against a `sudo tsng up`, over the same socket. ## What the agent does to the system @@ -74,7 +74,7 @@ off, or the agent stops: it wrote, never one you created by hand. - while broadcast is on, a `255.255.255.255` **route** through the overlay interface. The matching inbound-UDP firewall allowance is **not** implemented - yet (it needs a `pf` anchor); `tsunagi status` reports that half as incomplete + yet (it needs a `pf` anchor); `tsng status` reports that half as incomplete and names what to allow by hand if your host firewall drops discovery replies. Nothing taken from a remote peer ever becomes a path, a command argument or an diff --git a/dist/macos/cy.hexor.tsunagi.plist b/dist/macos/cy.hexor.tsunagi.plist index e267556..5447078 100644 --- a/dist/macos/cy.hexor.tsunagi.plist +++ b/dist/macos/cy.hexor.tsunagi.plist @@ -10,7 +10,7 @@ root:wheel with mode 0644 — launchd refuses a daemon plist that is group- or world-writable. See README.md in this directory. - Edit the binary path if tsunagi is not at /usr/local/bin/tsunagi. + Edit the binary path if tsng is not at /usr/local/bin/tsng. --> @@ -22,7 +22,7 @@ must not daemonise itself. --> ProgramArguments - /usr/local/bin/tsunagi + /usr/local/bin/tsng up diff --git a/dist/windows/README.md b/dist/windows/README.md index b988b0a..1cdef45 100644 --- a/dist/windows/README.md +++ b/dist/windows/README.md @@ -1,18 +1,18 @@ # Running tsunagi on Windows -This archive holds `tsunagi.exe`. The overlay interface is a Wintun adapter, so +This archive holds `tsng.exe`. The overlay interface is a Wintun adapter, so the agent needs two things to carry real traffic: 1. **`wintun.dll` beside the executable.** Download it from , take the DLL for your architecture (`amd64`), and - put it in the same folder as `tsunagi.exe`. + put it in the same folder as `tsng.exe`. 2. **An elevated process.** Creating the adapter requires Administrator rights. Open PowerShell or Command Prompt with **Run as administrator** and start the agent there: ```powershell -.\tsunagi.exe up +.\tsng.exe up ``` Run the commands that control it (`join`, `status`, `dns`, `network`) as the @@ -23,7 +23,7 @@ regardless of which account started the agent. ## Without touching the OS ```powershell -.\tsunagi.exe up --no-tun +.\tsng.exe up --no-tun ``` Tunnels form and handshake between agents, but no adapter, address or route is diff --git a/docs/architecture.md b/docs/architecture.md index d899195..7b5c5b5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,7 +69,9 @@ And in crates of their own: | crate | responsibility | |---|---| | `tsunagi-wg-quic` | the `wg-quic` protocol: its keys, its announcement, its tunnels | -| `tsunagi-cli` | the command line agent: the only place that owns a runtime, a logger and signals | +| `tsunagi` | the command line agent, built as the `tsng` executable: the only place that owns a runtime, a logger and signals | + +The core library is the package `tsunagi-core`; its library target is still called `tsunagi`, so code says `use tsunagi::…`. A protocol is a separate crate so the boundary is the compiler's to enforce, and so it can carry its own version. That version is not what peers compare: diff --git a/docs/broadcast.md b/docs/broadcast.md index b7a3a8c..6db0cca 100644 --- a/docs/broadcast.md +++ b/docs/broadcast.md @@ -6,11 +6,11 @@ each member advertises whether it accepts broadcasts over its authenticated control session. It is not a network-wide vote or signed shared configuration. ```sh -tsunagi join -n games --no-broadcast -tsunagi join -n games --broadcast -tsunagi network broadcast games off -tsunagi network broadcast games on -tsunagi network broadcast games +tsng join -n games --no-broadcast +tsng join -n games --broadcast +tsng network broadcast games off +tsng network broadcast games on +tsng network broadcast games ``` Joining without either flag preserves the saved choice. The `network broadcast` @@ -83,7 +83,7 @@ announcement. A rule left by a crashed run is replaced on the next apply. If `iptables` is missing (for example a host with only `nft`) or the process is not privileged, the route is still installed and `status` prints a `broadcast rules` line saying which half is missing and why. To undo it all, -turn broadcast off for the network: `tsunagi network broadcast off`. +turn broadcast off for the network: `tsng network broadcast off`. **One interface, several networks.** The host has one `255.255.255.255` route per interface, so only one overlay address can be its source. The lowest network diff --git a/docs/exit-node.md b/docs/exit-node.md index ff3f943..5b03fa8 100644 --- a/docs/exit-node.md +++ b/docs/exit-node.md @@ -9,7 +9,7 @@ or Tailscale's exit nodes work. Linux only for now. The offering side, per network, off by default: ```sh -tsunagi network exit-node on # or off +tsng network exit-node on # or off ``` Rules are installed even when the kernel does not forward yet, and `status` @@ -22,9 +22,9 @@ sudo sysctl -w net.ipv4.ip_forward=1 The using side: ```sh -tsunagi exit-node # who offers one -tsunagi exit-node # send everything through it -tsunagi exit-node off +tsng exit-node # who offers one +tsng exit-node # send everything through it +tsng exit-node off ``` The tray does the same: a checkbox on the network tile to offer, and an icon on @@ -65,7 +65,7 @@ be an exit node in one network and not in another. would otherwise cover everything you run. A per-user agent refuses to use an exit node and says why. - **If the exit node disappears the rules stay** and traffic is dropped until - it returns or you run `tsunagi exit-node off`. This is deliberate: falling + it returns or you run `tsng exit-node off`. This is deliberate: falling back to the direct path would leak traffic nobody agreed to send in the clear. The agent logs it once, `status` and the tray say so loudly. - **IPv6-only destinations are unreachable** meanwhile; carrying IPv6 through diff --git a/docs/mainline-dht.md b/docs/mainline-dht.md index 2cdf199..d43ec2f 100644 --- a/docs/mainline-dht.md +++ b/docs/mainline-dht.md @@ -1,6 +1,6 @@ # Mainline rendezvous -`tsunagi up` enables public Mainline DHT discovery. `--no-dht` disables it; +`tsng up` enables public Mainline DHT discovery. `--no-dht` disables it; `--dht` explicitly selects the default. `--reach local` always disables it. Manual `--peer` entries work alongside DHT or on their own. Library callers opt in with `AgentConfig::with_dht(MainlineDiscovery::default())`; merely @@ -86,5 +86,5 @@ Public DHT and relay checks are opt-in and are not part of the offline suite. Run the optional public check with: ```sh -cargo test --locked -p tsunagi --test mainline public_dht_finds_and_authenticates_two_agents -- --ignored --exact +cargo test --locked -p tsunagi-core --test mainline public_dht_finds_and_authenticates_two_agents -- --ignored --exact ``` diff --git a/docs/routing.md b/docs/routing.md index 6963f35..13762f1 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -75,7 +75,7 @@ neither administrative rights nor public relays/DHT. Run the forwarding microbenchmark explicitly: ```sh -cargo test --release -p tsunagi --lib forwarding_benchmark -- --ignored --nocapture +cargo test --release -p tsunagi-core --lib forwarding_benchmark -- --ignored --nocapture ``` It processes one million 64-byte frames and one million 1280-byte frames with diff --git a/docs/sync-model.md b/docs/sync-model.md index 5198bcf..9f3f286 100644 --- a/docs/sync-model.md +++ b/docs/sync-model.md @@ -1,6 +1,6 @@ # Planned state synchronisation -**The first slice of this model is now implemented**, in `crates/tsunagi/src/state/`, and is +**The first slice of this model is now implemented**, in `crates/tsunagi-core/src/state/`, and is used for one thing: IPv4 overlay addresses. What follows describes the whole model; the section at the end says exactly which parts exist. @@ -77,7 +77,7 @@ migrations for this. ## What exists today -Implemented, in `crates/tsunagi/src/state/`: +Implemented, in `crates/tsunagi-core/src/state/`: * signed records, one per author per network, each holding that author's complete current statement rather than a delta; diff --git a/docs/testing.md b/docs/testing.md index 4291987..04de202 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -89,7 +89,7 @@ The ignored `forwarding_benchmark` measures the synchronous transit routine in release mode, excluding crypto and socket I/O. Run it explicitly as described in [routing.md](routing.md); it has no timing threshold in the default suite. -Unit tests in `crates/tsunagi/src/state/` cover the signed record model directly: tampering +Unit tests in `crates/tsunagi-core/src/state/` cover the signed record model directly: tampering with any field breaks verification, a newer version wins while an older one never rolls back, two authors claiming one address resolve the same way no matter the merge order, one key used in two places is reported rather than @@ -103,7 +103,7 @@ asking a running agent for status over a real Unix socket, joining and leaving a network through it, a leftover socket file being replaced while a live one is not, and the derived socket path staying short enough to bind. -`crates/tsunagi-cli/tests/network_cli.rs` runs the real binary too: joining +`crates/tsunagi/tests/network_cli.rs` runs the real binary too: joining with no secret invents one, prints it in full and prints a line the other side can paste unchanged; a bare name this device already knows resumes that network instead of inventing another of the same name; and a network @@ -114,7 +114,7 @@ duplicate and unknown names change nothing and full ids resolve ambiguity. Selector unit tests cover case-sensitive names, Unicode/ellipsis names, copied id prefixes and collisions between names and ids without revealing secrets. -`crates/tsunagi-cli/tests/dns_service.rs` runs the real binary: the resolver +`crates/tsunagi/tests/dns_service.rs` runs the real binary: the resolver comes up with no interface to attach it to, the listener is not rebuilt on the way past, a name outside every zone is refused, each network gets a zone of its own as it is joined, and the resolver can be switched on and off @@ -128,7 +128,7 @@ about arrive as candidates that still pass the handshake like any other. candidate is enough to join, several backends compose, entries are withdrawn when a network stops, and a forgotten network stays forgotten across a restart. -Unit tests in `crates/tsunagi/src/proto/handshake.rs` cover the transcript construction +Unit tests in `crates/tsunagi-core/src/proto/handshake.rs` cover the transcript construction itself: role separation, channel binding, identity and network binding, unambiguous encoding, and rejection under the wrong key. diff --git a/docs/wireguard.md b/docs/wireguard.md index 55a319e..8070f70 100644 --- a/docs/wireguard.md +++ b/docs/wireguard.md @@ -58,7 +58,7 @@ and no second set of NAT problems to solve for WireGuard. ## Checking it from outside -`tsunagi status` asks a running agent over its local control socket and prints +`tsng status` asks a running agent over its local control socket and prints what it sees, including whether each tunnel has actually handshaken. See [../README.md](../README.md#checking-that-it-works). @@ -209,8 +209,8 @@ edit. Reconciliation is purely "do the running tunnels match what is known". ```bash # On both machines: the agent, then the network -tsunagi up -tsunagi join --network lab --secret "$SECRET" +tsng up +tsng join --network lab --secret "$SECRET" ``` See the two-machine walkthrough in [../README.md](../README.md#trying-it-on-two-machines).