Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
c3556c6
chore(dht): wire live Nat sampler + remote_address accessor
eshork May 15, 2026
5496afc
feat(peeroxide-dht): track and expose announce relay addresses
eshork May 15, 2026
8ec03f9
fix(peeroxide,peeroxide-dht): use client-advertised address for serve…
eshork May 15, 2026
1ca5453
fix(peeroxide-dht): synchronously proxy relayed PEER_HANDSHAKE / PEER…
eshork May 15, 2026
260906d
fix(peeroxide-dht): tag relay-forwarded REPLY with peer_address so cl…
eshork May 15, 2026
68e3ca4
feat(peeroxide-dht): accumulate per-record relay addresses during ann…
eshork May 15, 2026
c66b92b
fix(peeroxide): release server connection slot on stream establishmen…
eshork May 15, 2026
da36fcc
Revert "feat(peeroxide-dht): accumulate per-record relay addresses du…
eshork May 15, 2026
62cd96f
Reapply "feat(peeroxide-dht): accumulate per-record relay addresses d…
eshork May 15, 2026
985a369
Revert "Reapply "feat(peeroxide-dht): accumulate per-record relay add…
eshork May 15, 2026
5a0ddf9
fix(peeroxide): stop using LOOKUP-responder as PEER_HANDSHAKE relay f…
eshork May 15, 2026
4be57d0
Reapply "Reapply "feat(peeroxide-dht): accumulate per-record relay ad…
eshork May 15, 2026
b7e23ba
revert(peeroxide-dht): stop writing closest-acks into stored peer.rel…
eshork May 15, 2026
dd0a695
fix(peeroxide): stop synthesising 127.0.0.1 announce relay; use serve…
eshork May 15, 2026
eb994a5
fix(peeroxide): self-announce hash(pk) before topic-announce; adverti…
eshork May 15, 2026
5ba9112
feat(peeroxide-dht): try FIND_PEER FE-holders before FIND_NODE in con…
eshork May 15, 2026
0f22f35
perf(peeroxide): parallelize topic-announce and self-announce on hash…
eshork May 15, 2026
8f98e0b
feat(peeroxide-dht): per-request timeout/retries override on UserRequ…
eshork May 15, 2026
d9fa3d3
fix(peeroxide-dht,peeroxide): use FROM_SERVER mode + tid-preserved re…
eshork May 15, 2026
d8f49ca
feat(peeroxide,peeroxide-dht): advertise loopback in addresses4 + LAN…
eshork May 15, 2026
d8565f7
feat(peeroxide-dht): client advertises loopback in noise payload addr…
eshork May 15, 2026
03a1fce
feat(libudx): add UdxStream::set_firewall_hook (FnOnce single-fire)
eshork May 15, 2026
ead607e
feat(libudx+peeroxide): buffer pre-hook writes; wire server-side fire…
eshork May 15, 2026
620f65a
fix(peeroxide): advertise primary_socket port (not local_port) in add…
eshork May 15, 2026
306bfcf
fix(peeroxide-dht): mark UserRequest, UserRequestParams, RequestParam…
eshork May 15, 2026
e73a408
fix(peeroxide-dht): gate current_relay_addresses update to self-annou…
eshork May 15, 2026
5c19ba3
fix(peeroxide): defer dial attempt until RefreshComplete to avoid rel…
eshork May 15, 2026
18c5688
feat(peeroxide-dht,peeroxide): add ConnectOpts + SwarmConfig::local_c…
eshork May 15, 2026
ae69374
test(peeroxide-cli): add test_live_cp_send_recv_no_lan non-LAN gate
eshork May 15, 2026
18af55f
refactor(peeroxide-dht): replace Rust FE-holder dual-dispatch with ti…
eshork May 15, 2026
0708ce2
fix(libudx): harden firewall hook 4-tuple commit against implausible …
eshork May 15, 2026
70ea9d3
feat(peeroxide-dht): hoist passive holepunch reply builder into share…
eshork May 16, 2026
cc43ce9
feat(peeroxide-dht): replace fixed 2-round client holepunch with Node…
eshork May 16, 2026
7f93f5e
feat(peeroxide): wire passive Holepuncher into swarm server-side hand…
eshork May 16, 2026
2edc532
fix(peeroxide-dht): use NAT-derived same_host for direct-connect deci…
eshork May 16, 2026
f9c0bef
feat(peeroxide-dht): add SocketRef::take_holepunch_rx accessor
eshork May 16, 2026
ea22702
feat(peeroxide-dht): add udx_socket field to ConnectResult
eshork May 16, 2026
6c747db
feat(peeroxide-dht): spawn Holepuncher recv adapter for probe demux/r…
eshork May 16, 2026
c4dd6af
feat(peeroxide-dht): carry puncher socket through ConnectResult to UD…
eshork May 16, 2026
25e24bc
refactor(peeroxide-dht): apply item-8 tid-preserved relay pattern to …
eshork May 16, 2026
6abc520
chore(peeroxide-dht): trace inbound holepunch probes and recv-adapter…
eshork May 16, 2026
ce22ce0
feat(peeroxide-dht): feed Holepuncher::nat from PEER_HOLEPUNCH probe …
eshork May 16, 2026
d442174
feat(peeroxide-dht): add Nat::analyzing() resolve-on-settle primitive
eshork May 16, 2026
5160406
feat(peeroxide-dht): Holepuncher::analyze awaits Nat::analyzing for s…
eshork May 16, 2026
b9fbe2b
fix(peeroxide-dht,peeroxide): unblock Phase 3 holepunch on real publi…
eshork May 16, 2026
157c044
test(peeroxide-cli): flip no-LAN cp test to expected-success post-Pha…
eshork May 16, 2026
7c5998b
fix(peeroxide,peeroxide-dht): passive holepunch reply must advertise …
eshork May 16, 2026
5640f05
fix(peeroxide-dht): mark SecretstreamError and UserQueryParams non_ex…
eshork May 17, 2026
e9f2a5b
Add visibility policy and audit rubric
eshork May 17, 2026
9696b72
Verify FROM_PRIOR_RECON dispositions with full reachability data
eshork May 17, 2026
0636819
refactor(peeroxide-dht): visibility reform Phase 1 (promote 7 modules…
eshork May 17, 2026
260037b
fix(peeroxide-dht): close Phase 1 cascade leak and restore blind_rela…
eshork May 17, 2026
e2a3fc6
feat(peeroxide-dht): Phase 2 u2014 apply #[non_exhaustive] to remaini…
eshork May 17, 2026
8ee0c9d
fix(peeroxide-dht): close Phase 2 cascade gaps from Oracle review
eshork May 17, 2026
e28d59a
docs(peeroxide-dht,peeroxide-cli): Phase 4 u2014 module docs + CHANGE…
eshork May 17, 2026
c4d99fa
docs(visibility): close DoD #8 with explicit skip justifications for …
eshork May 17, 2026
00a550a
docs(visibility): correct §11.4 skip-justification enumeration per Or…
eshork May 17, 2026
3de707d
docs(visibility): add CongestionWindow to §11.4 Category B per Oracle…
eshork May 17, 2026
6ee1fe8
docs(peeroxide-dht): restore secretstream module-level libsodium notes
eshork May 18, 2026
f9336e0
docs(visibility): trim VISIBILITY_POLICY.md to forward-facing rubric
eshork May 18, 2026
b4b935e
chore(peeroxide-dht): bump to 1.4.0
eshork May 18, 2026
1523c3a
test(peeroxide-dht): autoSample contract scaffolding (red)
eshork May 18, 2026
73fcb4d
feat(peeroxide-dht): RoutingTable::recent for autoSample target selec…
eshork May 18, 2026
470e73b
feat(peeroxide-dht): parse-based DHT-reply demux on puncher sockets
eshork May 18, 2026
0b934cc
feat(peeroxide-dht): actor-routed puncher-socket request/reply plumbing
eshork May 18, 2026
92ee478
feat(peeroxide-dht): DhtHandle::ping_via_socket crate-private
eshork May 18, 2026
d5de10b
feat(peeroxide-dht): Nat::auto_sample orchestration
eshork May 18, 2026
bb6a254
feat(peeroxide-dht,peeroxide): drive auto_sample on initiator and pas…
eshork May 18, 2026
ba1d2bf
fix(peeroxide-dht): revert coerce_firewall UNKNOWN coercion (Node par…
eshork May 18, 2026
bbe6cd5
test(peeroxide-cli): remove obsolete fresh-NAT placeholder
eshork May 18, 2026
fa35269
docs(peeroxide): clarify swarm passive auto_sample ordering rationale
eshork May 18, 2026
b10c6e4
feat(peeroxide-dht): add Holepuncher::local_addresses for Node-parity…
eshork May 18, 2026
b16aa42
feat(peeroxide-dht): add Holepuncher::punch_addresses (autoSample + L…
eshork May 18, 2026
b1e74cd
feat(peeroxide-dht): add HyperDhtHandle::noise_addresses4 (remote + l…
eshork May 18, 2026
3a8e93f
fix(peeroxide-dht): use real LAN interfaces in noise addresses4 and h…
eshork May 18, 2026
8bcfe65
fix(peeroxide): advertise real LAN addresses in noise reply and holep…
eshork May 18, 2026
35062b8
fix(peeroxide-dht): combine reflexive + LAN in punch_addresses
eshork May 18, 2026
d08712a
feat(peeroxide-cli): add PEEROXIDE_FORCE_RELAY env var
eshork May 19, 2026
80abdd0
refactor(tracing): adopt peeroxide::_events::* lifecycle target conve…
eshork May 19, 2026
ea89d6b
fix(peeroxide-dht,libudx): close remaining stub/gap TODOs
eshork Jul 12, 2026
0197ab2
fix(tests/docker): redesign NAT holepunch rig to one DHT node per con…
eshork Jul 12, 2026
b7404fa
feat(peeroxide-dht,peeroxide-cli): implement blind-relay server
eshork Jul 12, 2026
eec3df8
feat(peeroxide-dht): idle-session-timeout, stream teardown, Node rela…
eshork Jul 12, 2026
531eb19
fix(peeroxide-dht): self-announce relay identity so Node clients can …
eshork Jul 12, 2026
5da6d46
docs: update CHANGELOGs for blind-relay feature and self-announce fix
eshork Jul 12, 2026
3e0e5e9
fix(peeroxide-dht): mark RelayStats non_exhaustive per VISIBILITY_POL…
eshork Jul 12, 2026
ca5d5e9
docs(AGENTS,CONTRIBUTING): wire VISIBILITY_POLICY.md into standard wo…
eshork Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Keep Docker build contexts (tests/docker/Dockerfile.*) small — these
# build with `context: ../../` (the repo root) so `COPY . .` in
# Dockerfile.rust-peer must not drag in local build artifacts or
# unrelated tooling directories.
/target/
tests/node/node_modules/
.git/
.claude/
.sisyphus/
.vogon_poetry/
docs/book/
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ peeroxide — topic-based peer discovery + connection management
- Adding optional parameters via a new companion function
- Internal implementation changes with no public surface effect

### New public API surface — always apply VISIBILITY_POLICY.md

Adding new "does NOT count as breaking" surface above is still not free of process: any new `pub` type, function, or module in `libudx`, `peeroxide-dht`, or `peeroxide` must be checked against [`VISIBILITY_POLICY.md`](./VISIBILITY_POLICY.md) **in the same change**, not as an optional follow-up — do this automatically, without waiting to be asked:

- Decide `pub` vs `pub(crate)` per the policy's Axis 1 rubric.
- If `pub`, classify it into a type-role (Handle / Config / Event-Result / Error / Primitive / Wire-envelope) and apply that role's `#[non_exhaustive]` default. The burden of proof is on exemptions — don't skip `#[non_exhaustive]` on a Config/Result/Error/Event-role type without a documented reason.
- `#[non_exhaustive]` Config/Options types must ship a `::new(...)`/builder/`Default` impl in the same commit (consumers must be able to construct them).
- New `pub mod` needs real module-level docs in the same commit, or it should be `pub(crate) mod` instead.

This check belongs in the same pass as writing the code, before it's ever proposed as done — treat it like running clippy, not like a separate audit someone else requests later.

### The boundary that matters

`peeroxide-cli` is a binary consumer. Constraints it places on how it uses library internals (e.g. needing to share a socket across tasks) must be solved **inside `peeroxide-cli` or by adding new non-breaking API**, never by altering existing library signatures.
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,16 @@ These three library crates are published at `>=1.0` and have external users.

When in doubt, add a new function rather than changing an existing one.

For *new* public types/functions/modules, see [`VISIBILITY_POLICY.md`](./VISIBILITY_POLICY.md) for the visibility (`pub` vs `pub(crate)`) and `#[non_exhaustive]` rubric — it exists precisely so future additive changes (new fields, new variants) don't become breaking changes later.

## PR Checklist

### Before opening a PR

- [ ] Both test suites pass locally (see Testing above)
- [ ] Clippy clean
- [ ] No public API breaking changes without maintainer approval
- [ ] New/changed public API in `libudx`, `peeroxide-dht`, or `peeroxide` reviewed against `VISIBILITY_POLICY.md` (visibility + `#[non_exhaustive]` per the type-role taxonomy)
- [ ] CHANGELOG updated for each affected crate
- [ ] Version bumps are correct per semver (patch / minor / major)
- [ ] New public API items have `///` doc comments
Expand Down
19 changes: 15 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 122 additions & 0 deletions VISIBILITY_POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Visibility Policy

How to decide whether a new type, function, or module in `libudx`, `peeroxide-dht`, or `peeroxide` should be `pub`, `pub(crate)`, or `#[non_exhaustive]`.

`peeroxide-cli` is a binary consumer of these crates; it adapts to the library API, not the other way around.

## Why this exists

Routine bug fixes and feature gap-fills should not trigger SemVer-breaking diffs. They do whenever something is `pub` that shouldn't be, or whenever a public Config/Result/Event lacks `#[non_exhaustive]` and we want to add a field. This policy is the rubric we apply to keep the published API surface intentional and additively extensible.

## Goals

1. Identify the complete public API surface — every reachable item.
2. Keep non-surface entities `pub(crate)` so internal changes don't trigger SemVer churn.
3. Enable additive evolution of the surviving surface (new fields, new variants, new methods) without breaking the SemVer contract.
4. Stay compatible with the broader Hyperswarm / Hypercore / Holepunch ecosystem (see "Reference ecosystem pins" below).

## Non-goals

- Preserving struct-literal constructability for downstream users.
- Backward compatibility for items we explicitly demote.
- Wire-protocol changes (envelopes' Rust shape is downstream of the wire format, not the API contract).

## The two-axis decision

For every public-ish item, make two independent decisions.

### Axis 1 — visibility (`pub` vs `pub(crate)`)

In order of precedence:

1. Is it reached from outside its defining crate today (workspace-internal cross-crate use counts)? → `pub`.
2. Is it on the **pin list** below? → `pub`.
3. Is it returned by, or required as a parameter of, a public function in the same crate? → `pub` (cascade reachability).
4. Is it a wire-format envelope used cross-crate within the workspace? → `pub`.
5. Otherwise → `pub(crate)`.

### Axis 2 — `#[non_exhaustive]` (only if Axis 1 = `pub`)

1. The role default from the **type-role taxonomy** below applies unless explicitly carved out.
2. Default is **apply**. The burden of proof is on exemptions.

**Constructor rule**: every `#[non_exhaustive]` type that users construct (Configs, Options, Params, HandlerReply-shaped types) must ship a `::new(...)` / builder / `Default` impl in the same commit. Otherwise consumers can't construct it.

## Type-role taxonomy

Classify every public-ish item into exactly one role. The role drives both defaults.

| Role | Visibility default | `#[non_exhaustive]` default | Examples |
|---|---|---|---|
| **Handle** — opaque internal-state-with-methods, factory-constructed, never struct-literal'd | `pub` | no (adds nothing; consumers can't struct-literal a private-field type anyway) | `HyperDhtHandle`, `SwarmHandle`, `DhtHandle`, `Io`, `CongestionWindow`, `Holepuncher`, `SecretStream<T>`, `Push`, `Pull`, `SecurePayload`, `SocketPool`, `NoiseWrap`, `BlindRelayClient`, `Persistent`, `Router`, `UdxRuntime`, `UdxStream`, `UdxSocket`, `UdxAsyncStream` |
| **Config / Options / Params** — user-constructed for input | `pub` | **yes** (forward-compat for new knobs) | `SwarmConfig`, `JoinOpts`, `HyperDhtConfig`, `ServerConfig`, `ConnectOpts`, `IoConfig`, `PersistentConfig`, `WireCounters` |
| **Event / Result / Reply** — produced by the library, matched/destructured by the user | `pub` | **yes** (forward-compat for new fields/variants) | `LookupResult`, `AnnounceResult`, `ConnectResult`, `ImmutablePutResult`, `MutablePutResult`, `MutableGetResult`, `ServerEvent`, `IoEvent`, `TimeoutEvent`, `HolepunchEvent`, `ChannelEvent`, `PairResponse`, `HandshakeResult`, `PeerConnection`, `SwarmConnection`, `PeerInfo` |
| **Error** — matched by the user | `pub` | **yes** (forward-compat for new variants) | `HyperDhtError`, `SwarmError`, `UdxError`, `SecretStreamError`, `SecretstreamError`, `RelayError`, `ProtomuxError`, `IoError`, `NoiseError`, `EncodingError` |
| **Primitive / value type** — small, widely-constructed, semantics stable | `pub` | no (would block legitimate value construction) | `KeyPair`, `PeerAddr`, `noise::Keypair`, `Datagram`, `Priority` |
| **Wire-format envelope** — Rust shape mirrors a serialized protocol message | `pub` if cross-crate, else `pub(crate)` | **no** (struct-literal construction IS the protocol implementation) | All of `messages::*`, `hyperdht_messages::*`, `protomux::{ControlFrame, BatchItem, DecodedFrame}`, `blind_relay::{PairMessage, UnpairMessage}`, `noise::{Handshake, HandshakeIK}`, `compact_encoding::State` |
| **Public free function** | `pub` only if part of public contract; else `pub(crate)` | n/a (functions don't take `#[non_exhaustive]` — use signature discipline) | `discovery_key`, `crypto::{hash, hash_batch, sign_detached, verify_detached, namespace}` |
| **Trait** | case-by-case; use sealed-trait pattern (private supertrait or `Sealed` marker) if no out-of-crate impls are intended | n/a | — |
| **Internal helper** — never reached from outside the defining crate | `pub(crate)` | n/a | NAT state machine, routing-table internals, encoder primitives in `compact_encoding`, demoted `blind_relay::encode_*`/`decode_*` helpers |

## Reference ecosystem pins

These types MUST remain `pub`. They are the surface a future Rust port of any Hyperswarm-family project (hyperbeam, `@hyperswarm/rpc`, hypercore replication, etc.) would need to import. Verified against ~150 holepunchto-org projects.

**peeroxide**:
- `spawn()`, `discovery_key()`
- `SwarmConfig`, `JoinOpts`, `SwarmHandle`, `SwarmConnection`
- `SwarmHandle::{join, leave, dht, key_pair}`

**peeroxide-dht**:
- `spawn` (in `hyperdht` and `rpc`)
- `HyperDhtConfig`, `ServerConfig`, `ConnectOpts`
- `KeyPair` (with `from_seed` / `generate`)
- `HyperDhtHandle`, `PeerConnection`, `Holepuncher`
- `HyperDhtHandle::{lookup, announce, connect, connect_with_options, connect_with_nodes, connect_to, find_peer, mutable_put, mutable_get, immutable_put, immutable_get}`
- `LookupResult`, `AnnounceResult`, `ConnectResult`, `ImmutablePutResult`, `MutablePutResult`, `MutableGetResult`, `ServerEvent`
- `secret_stream::SecretStream`, `noise_wrap::NoiseWrap`, `protomux::*`
- `crypto::{discovery_key, hash, hash_batch, sign_detached, verify_detached, namespace}`

**libudx**:
- `UdxRuntime`, `UdxSocket`, `UdxStream`, `UdxAsyncStream`
- `UdxRuntime::{create_socket, create_stream}`
- `UdxSocket::{bind, send_to, recv_start, close}`
- `Header` and the wire flag constants (`FLAG_DATA`, `FLAG_END`, etc.)

## When the rubric is ambiguous

- **Adding a new public type?** Apply the role taxonomy. Default to `#[non_exhaustive]` for Config / Result / Event / Error roles. Skip for Handles, Primitives, and wire-format envelopes.
- **Adding a new module?** Default `pub(crate) mod`. Promote to `pub mod` only if it's a documented advanced-use surface — and only if it gets real module-level docs in the same commit.
- **Tempted to use `#[doc(hidden)] pub mod`?** Don't. It's a footgun: the module is publicly reachable but absent from rustdoc, so reviewers miss leaks. Either it's `pub mod` (with real docs) or it's `pub(crate) mod`.
- **Removing an item?** Breaking change. Requires explicit human approval per AGENTS.md HARD STOP.
- **Adding a field to a non-`#[non_exhaustive]` struct?** Breaking change. Requires approval. (Reconsider whether the struct should have been `#[non_exhaustive]` in the first place.)
- **Changing an existing public signature?** Breaking change. Requires approval.

## How to extend this policy

If a new use case doesn't fit cleanly into an existing role, prefer either:

1. **Carve-out**: prove it's an exception to a clear role and document the carve-out inline in the relevant role row above.
2. **New role**: introduce a new row in the type-role taxonomy with explicit defaults.

Don't ad-hoc decide visibility per-type without updating this document.

## Auditing the current state

When the question "is the public surface still consistent with this policy?" comes up:

1. Generate the public-surface inventory:
```bash
RUSTC_BOOTSTRAP=1 cargo rustdoc -p <crate> -- \
-Z unstable-options --output-format json --document-hidden-items
```
(The `--document-hidden-items` flag is required to surface `#[doc(hidden)] pub mod` content if any has crept back in.)
2. For each public `struct` / `enum`, confirm it's covered by the role taxonomy above (either `#[non_exhaustive]` is applied per its role's default, or its role exempts it).
3. For each `pub mod`, confirm it has real module-level documentation. No `#[doc(hidden)] pub mod`.
4. For cross-crate reachability questions, use qualified-path grep (`use peeroxide_dht::<mod>`, `peeroxide_dht::<mod>::`). Do NOT rely on name-only graph queries — common method names like `destroy`, `add`, `update` create false positives.

## Hard constraints (from AGENTS.md, restated here for convenience)

- **No `git push`** without explicit user direction.
- **API breaking change HARD STOP**: visibility demotions (`pub` → `pub(crate)`) and removal of items are breaking changes. They require explicit human approval — they are not part of "routine maintenance."
- **MSRV**: Rust 1.85 (2024 edition).
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@

- [Security Model](./appendices/security-model.md)
- [Limits and Performance](./appendices/limits-and-performance.md)
- [Tracing and Logging](./appendices/tracing.md)
138 changes: 138 additions & 0 deletions docs/src/appendices/tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Tracing and Logging

Peeroxide uses the [`tracing`](https://docs.rs/tracing) crate for all
diagnostic output across its four crates (`libudx`, `peeroxide-dht`,
`peeroxide`, `peeroxide-cli`). This appendix documents the conventions
operators and developers can rely on when filtering, capturing, or
extending log output.

## Target conventions

Every tracing call has a *target* — a string used by the
[`EnvFilter`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)
to decide whether to emit the event. Peeroxide uses two kinds of
targets:

**1. Module-path targets (default).** Most calls inherit their target
from the Rust module path: `peeroxide_dht::holepuncher`,
`peeroxide_dht::hyperdht`, `libudx::native::stream`, and so on. These
are the natural granularity for developers debugging a specific
subsystem. EnvFilter directives prefix-match, so
`peeroxide_dht::holepuncher=trace` enables that one module and its
children.

**2. Reserved `peeroxide::_events::*` lifecycle targets.** A curated set
of high-signal operator-facing events use a stable, hand-picked target
under `peeroxide::_events::`. These are not Rust modules — they are
fixed labels that survive refactors. Examples:

```text
peeroxide::_events::swarm::started
peeroxide::_events::dht::bootstrapped
peeroxide::_events::peer::connected
peeroxide::_events::peer::connect_failed
peeroxide::_events::holepunch::probe_received
peeroxide::_events::holepunch::passive_reflected
peeroxide::_events::holepunch::nat_settled
peeroxide::_events::holepunch::final_punch_sent
peeroxide::_events::holepunch::connected
peeroxide::_events::holepunch::aborted
peeroxide::_events::holepunch::failed_no_verified_addr
```

Operators tail these to get a clean lifecycle stream without developer
noise:

```sh
RUST_LOG=peeroxide::_events=info peeroxide cp send ./file
```

## Level discipline

| Level | Used for | Default visibility |
|---|---|---|
| `error` | Fatal conditions; the operation cannot proceed | always |
| `warn` | Recoverable anomalies, validation failures, retries | always |
| `info` | Lifecycle events (`peeroxide::_events::*`) + startup | `-v` and above |
| `debug` | Per-connection / per-round state transitions | `-vv` and above |
| `trace` | Per-packet, per-loop iteration | only with explicit `RUST_LOG` |

Anything that fires more than once per significant operation lives at
`debug` or below. Per-packet paths live at `trace`. The `info` level is
reserved for the `_events::*` subtree plus a small handful of true
startup events.

## CLI verbosity

The `peeroxide` CLI exposes three verbosity levels via the `-v` flag,
each composing a default `EnvFilter`:

| Flag | Default filter | What you see |
|---|---|---|
| _(none)_ | `warn,peeroxide::_events=info` | Warnings + lifecycle events |
| `-v` | `peeroxide=info,peeroxide_dht=info,peeroxide::_events=info,warn` | Info-level developer events across both swarm and DHT crates |
| `-vv` | `peeroxide=debug,peeroxide_dht=debug,libudx=debug,peeroxide::_events=info,info` | Full debug stream across all peeroxide crates |

The `RUST_LOG` environment variable always overrides the default. Any
EnvFilter directive syntax is supported:

```sh
RUST_LOG=peeroxide_dht::holepuncher=trace peeroxide cp send ./file
RUST_LOG=peeroxide::_events=info,libudx=warn peeroxide cp recv <topic> -
RUST_LOG=peeroxide_dht::hyperdht=trace,peeroxide_dht::io=debug peeroxide node
```

## Subsystem map

The 8 natural subsystems and the targets that feed them:

| Subsystem | Module path | Event subtree |
|---|---|---|
| holepunch | `peeroxide_dht::holepuncher` | `peeroxide::_events::holepunch::*` |
| nat | `peeroxide_dht::nat` | _(none currently)_ |
| socket_pool | `peeroxide_dht::socket_pool` | _(none currently)_ |
| relay | `peeroxide_dht::blind_relay` | _(none currently)_ |
| discovery | `peeroxide_dht::query`, `peeroxide::peer_discovery` | _(none currently)_ |
| swarm | `peeroxide::swarm` | `peeroxide::_events::swarm::*`, `peeroxide::_events::peer::*` |
| dht_rpc | `peeroxide_dht::rpc`, `peeroxide_dht::io` | `peeroxide::_events::dht::*` |
| udx | `libudx::native::*` | _(none currently)_ |

New `_events::*` labels should be added sparingly, only when an event
represents an operator-visible lifecycle transition (something a
production operator would want in a clean default-level log).

## Adding a new lifecycle event

When wiring a new high-signal event, use an explicit target:

```rust
tracing::info!(
target: "peeroxide::_events::holepunch::nat_settled",
round,
"NAT settled + verified remote, transitioning to final punch round"
);
```

When the same site also wants developer-level detail at debug level,
emit two separate calls or include enough structured fields in the
single `info!` so it serves both audiences (the latter is preferred).

## Anti-patterns

- **`eprintln!` for telemetry.** Bypasses level filtering and structured
fields. Always use a tracing macro.
- **Emitting at `info` from a per-packet path.** Demote to `debug` or
`trace`; the `info` level is reserved for lifecycle events.
- **Inventing many ad-hoc targets.** Stick to module-path defaults
unless the call belongs to a curated `_events::*` lifecycle category.
- **Putting expensive computation outside the tracing macro.** The
`tracing` macros short-circuit on the level filter before evaluating
field expressions, so inline `format!()` / `.collect()` calls inside
the macro are gated. The same code as a `let` outside the macro
always runs.

## Reference

- [`tracing` crate](https://docs.rs/tracing)
- [`tracing-subscriber::EnvFilter`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)
- Inspired by [iroh's `iroh::_events::*` pattern](https://github.com/n0-computer/iroh)
6 changes: 6 additions & 0 deletions libudx/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- Fixed a `process_incoming` ordering bug: the relay fast-path (packet forwarding once `UdxStream::relay_to` is configured) was checked before the firewall-hook gate, permanently starving hook-based 4-tuple address adoption whenever `relay_to` was pre-wired on both sides of a pairing before either side's first packet arrived. The relay-forwarding check now runs after the firewall-hook gate.

## [1.3.1](https://github.com/Rightbracket/peeroxide/compare/libudx-v1.3.0...libudx-v1.3.1) - 2026-05-14

### Other
Expand Down
Loading
Loading