SessionDatagram redesign: add src_addr, reclassify error signals

Add src_addr to SessionDatagram envelope (34-byte header: msg_type +
src_addr + dest_addr + hop_limit) so transit routers can route error
signals back to the packet's originator.

Reclassify CoordsRequired/PathBroken as link-layer error signals
(plaintext inside SessionDatagram) rather than e2e encrypted session
messages. Transit routers generate these when forwarding fails and
route them to src_addr; if source is also unreachable, drop silently.

Remove redundant src_addr/dest_addr/hop_limit from SessionSetup,
SessionAck, and DataPacket (now in envelope). DataPacket header
reduced from 36 to 4 bytes. Remove PathBroken.original_src.

Fix routing loop vulnerability: gate bloom filter path on having
cached dest_coords to prevent blind forwarding between peers.
Simplify select_best_candidate() to require coordinates.

Fix gossip protocol type codes (0x11->0x20, 0x12->0x30, 0x13->0x31)
for consistency across all design docs.

All 5 design docs updated and cross-checked for consistency.
335 tests pass, zero warnings.
This commit is contained in:
Johnathan Corgan
2026-02-12 11:32:45 +00:00
parent 2f8e97c0ab
commit d41009b778
9 changed files with 535 additions and 349 deletions
+19 -18
View File
@@ -417,12 +417,13 @@ Within the link-layer message framing:
| Type Code | Message |
|-----------|---------|
| 0x10 | TreeAnnounce |
| 0x11 | FilterAnnounce |
| 0x12 | LookupRequest |
| 0x13 | LookupResponse |
| 0x20 | FilterAnnounce |
| 0x30 | LookupRequest |
| 0x31 | LookupResponse |
These are carried inside the encrypted link-layer payload after Noise IK
handshake completion.
handshake completion. See [fips-wire-protocol.md](fips-wire-protocol.md) §2.6
for the full link message type table.
---
@@ -556,13 +557,13 @@ ANCESTRY[3] - Root:
Total payload: 1 + 1 + 8 + 8 + 16 + 2 + (4 × 32) + 64 = 228 bytes
```
### A.2 FilterAnnounce (0x11)
### A.2 FilterAnnounce (0x20)
Propagates Bloom filter reachability information.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ FILTER ANNOUNCE (0x11) │
│ FILTER ANNOUNCE (0x20) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
@@ -570,7 +571,7 @@ Propagates Bloom filter reachability information.
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x11 │ │
│ │ 0 │ msg_type │ 1 byte │ 0x20 │ │
│ │ 1 │ sequence │ 8 bytes │ u64 LE, monotonic counter │ │
│ │ 9 │ hash_count │ 1 byte │ Number of hash functions (5) │ │
│ │ 10 │ size_class │ 1 byte │ Filter size: 512 << class │ │
@@ -615,7 +616,7 @@ Propagates Bloom filter reachability information.
```text
PLAINTEXT BYTES:
11 ← msg_type = FilterAnnounce
20 ← msg_type = FilterAnnounce
2A 00 00 00 00 00 00 00 ← sequence = 42
05 ← hash_count = 5
01 ← size_class = 1 (1 KB filter)
@@ -624,13 +625,13 @@ PLAINTEXT BYTES:
Total: 1035 bytes
```
### A.3 LookupRequest (0x12)
### A.3 LookupRequest (0x30)
Discovers tree coordinates for distant destinations.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ LOOKUP REQUEST (0x12) │
│ LOOKUP REQUEST (0x30) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
@@ -638,7 +639,7 @@ Discovers tree coordinates for distant destinations.
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x12 │ │
│ │ 0 │ msg_type │ 1 byte │ 0x30 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, unique identifier │ │
│ │ 9 │ target │ 16 bytes │ NodeAddr being searched for │ │
│ │ 25 │ origin │ 16 bytes │ NodeAddr of requester │ │
@@ -664,7 +665,7 @@ Discovers tree coordinates for distant destinations.
```text
PLAINTEXT BYTES:
12 ← msg_type = LookupRequest
30 ← msg_type = LookupRequest
[8 bytes request_id] ← random unique ID
[16 bytes target node_addr] ← who we're looking for
[16 bytes origin node_addr] ← who's asking
@@ -677,13 +678,13 @@ PLAINTEXT BYTES:
Total: 1 + 8 + 16 + 16 + 1 + 2 + 64 + 1 + 256 = 365 bytes
```
### A.4 LookupResponse (0x13)
### A.4 LookupResponse (0x31)
Returns target's coordinates to the requester.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ LOOKUP RESPONSE (0x13) │
│ LOOKUP RESPONSE (0x31) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
@@ -691,7 +692,7 @@ Returns target's coordinates to the requester.
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x13 │ │
│ │ 0 │ msg_type │ 1 byte │ 0x31 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, echoes request │ │
│ │ 9 │ target │ 16 bytes │ NodeAddr that was found │ │
│ │ 25 │ target_coords_cnt│ 2 bytes │ u16 LE │ │
@@ -717,7 +718,7 @@ Returns target's coordinates to the requester.
```text
PLAINTEXT BYTES:
13 ← msg_type = LookupResponse
31 ← msg_type = LookupResponse
[8 bytes request_id] ← echoed from request
[16 bytes target node_addr] ← confirms who was found
05 00 ← target_coords_count = 5
@@ -746,7 +747,7 @@ Source S wants to reach distant destination D (not in local filters)
│ ┌───────────┘ │
│ ▼ │
│ ┌──────┬───────────────────────────────────┐ │
│ │ 0x12 │ LookupRequest payload │ │
│ │ 0x30 │ LookupRequest payload │ │
│ │ │ (target=D, origin=S, ttl=8, ...) │ │
│ └──────┴───────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
@@ -765,7 +766,7 @@ Source S wants to reach distant destination D (not in local filters)
│ ┌───────────┘ │
│ ▼ │
│ ┌──────┬───────────────────────────────────┐ │
│ │ 0x13 │ LookupResponse payload │ │
│ │ 0x31 │ LookupResponse payload │ │
│ │ │ (target=D, coords=[D,P1,P2,Root]) │ │
│ └──────┴───────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
+23 -11
View File
@@ -462,28 +462,40 @@ FIPS uses a discriminator-based wire format for efficient message dispatch.
Exchanged between directly connected peers, encrypted with link session keys:
| Type | Name | Purpose |
|------|----------------|--------------------------------------------|
| 0x10 | TreeAnnounce | Spanning tree state (parent, ancestry) |
| 0x20 | FilterAnnounce | Bloom filter reachability update |
| 0x30 | LookupRequest | Query for node's tree coordinates |
| 0x31 | LookupResponse | Response with coordinates and proof |
| 0x40 | SessionDatagram| Carries end-to-end encrypted payloads |
| 0x50 | Disconnect | Orderly disconnect notification |
| Type | Name | Purpose |
|------|----------------|------------------------------------------------------|
| 0x10 | TreeAnnounce | Spanning tree state (parent, ancestry) |
| 0x20 | FilterAnnounce | Bloom filter reachability update |
| 0x30 | LookupRequest | Query for node's tree coordinates |
| 0x31 | LookupResponse | Response with coordinates and proof |
| 0x40 | SessionDatagram| Carries session-layer payloads through the mesh |
| 0x50 | Disconnect | Orderly disconnect notification |
### Session Layer Messages
### SessionDatagram Payload Messages
Carried inside SessionDatagram, encrypted end-to-end between source and
destination:
Carried inside SessionDatagram (0x40), which provides src_addr, dest_addr, and
hop_limit for multi-hop forwarding.
**Session-layer messages** (end-to-end encrypted between source and destination):
| Type | Name | Purpose |
|------|----------------|--------------------------------------------|
| 0x00 | SessionSetup | Establish routing session with coordinates |
| 0x01 | SessionAck | Acknowledge session establishment |
| 0x10 | DataPacket | Encrypted application data (IPv6 payload) |
**Link-layer error signals** (plaintext, generated by transit routers):
| Type | Name | Purpose |
|------|----------------|--------------------------------------------|
| 0x20 | CoordsRequired | Router cache miss—need fresh coordinates |
| 0x21 | PathBroken | Greedy routing failed—need re-lookup |
Error signals are sent back to the original source using the `src_addr` from
the SessionDatagram that triggered the error. They are plaintext because the
transit router has no end-to-end session with the source; link-layer encryption
protects them hop-by-hop.
See [fips-wire-protocol.md](fips-wire-protocol.md) for wire format details and
[fips-gossip-protocol.md](fips-gossip-protocol.md) for gossip message formats.
+104 -60
View File
@@ -418,16 +418,20 @@ No global routing tables. Each node makes purely local decisions.
### Privacy Considerations
Intermediate routers can observe `src_addr` and `dest_addr` in transiting packets.
This enables traffic analysis (who is communicating with whom) but not content
inspection (the payload is end-to-end encrypted with session keys).
Intermediate routers can observe `src_addr` and `dest_addr` in the
SessionDatagram envelope of transiting packets. This enables traffic analysis
(who is communicating with whom) but not content inspection (the payload is
end-to-end encrypted with session keys).
**Why source address is visible**: The source address is required for routers to
send error messages (CoordsRequired, PathBroken) back to the sender. This is a
deliberate design choice: rather than silently dropping unroutable packets and
relying on application-layer timeouts to detect failures, FIPS provides explicit
feedback that allows rapid route recovery. The tradeoff favors responsiveness
over metadata privacy.
**Why source address is visible**: The `src_addr` field in the SessionDatagram
is required for transit routers to send error signals (CoordsRequired,
PathBroken) back to the sender. When a transit router R cannot forward a
SessionDatagram `{src: S, dest: D}`, it creates a new SessionDatagram
`{src: R, dest: S}` carrying the error signal, and routes it toward S using
`find_next_hop(S)`. This is a deliberate design choice: rather than silently
dropping unroutable packets and relying on application-layer timeouts to detect
failures, FIPS provides explicit feedback that allows rapid route recovery.
The tradeoff favors responsiveness over metadata privacy.
**Partial mitigation**: FIPS addresses are derived from `SHA-256(pubkey)`, not the
npub itself. An observer learns that `fd12:3456:...` is communicating with
@@ -455,8 +459,9 @@ The coordinate cache serves two functions:
1. **Greedy routing fallback** — when bloom filters haven't converged,
cached coordinates enable tree-distance-based forwarding.
2. **Reduced packet overhead** — once coordinates are cached at intermediate
routers, data packets can carry addresses only (38 bytes) rather than
full coordinates (~300 bytes).
routers, data packets can use minimal DataPacket headers (4 bytes inside
a 34-byte SessionDatagram = 38 bytes total) rather than including full
coordinates (~170 bytes total).
### Cache Lifecycle
@@ -472,84 +477,101 @@ The coordinate cache serves two functions:
### Session Setup Flow
All messages are carried inside SessionDatagram envelopes that provide
`src_addr`, `dest_addr`, and `hop_limit` at the link layer.
```text
S R1 R2 D
│ │ │ │
│──SessionSetup─────────>│ │ │
(src_coords, │──SessionSetup────────>│ │
dest_coords) │ │──SessionSetup────────>│
│ │ │
│ cache: │ cache: │
│──SessionDatagram──────>│ │ │
{src:S, dest:D, │──SessionDatagram─────>│ │
payload:SessionSetup │ │──SessionDatagram─────>│
(src_coords, │ │ │
dest_coords)} │ cache: │ cache: │
│ │ dest_addr→dest_coords│ dest_addr→dest_coords│
│ │ src_addr→src_coords │ src_addr→src_coords │
│ │ │ │
│<─────────────────────────────────────────────────────────SessionAck──│
│<──────────────────────────────────────────SessionDatagram{SessionAck}──│
│ │ │ │
│══DataPacket═══════════>│══════════════════════>│══════════════════════>│
(addresses only) │ (use cached coords) │ (use cached coords) │
│══SessionDatagram══════>│══════════════════════>│══════════════════════>│
{src:S, dest:D, │ (use cached coords) │ (use cached coords) │
│ payload:DataPacket} │ │ │
```
### Router Behavior
Transit routers process SessionDatagram envelopes. The envelope provides
`src_addr` and `dest_addr` for routing decisions and error signaling.
```rust
impl Router {
fn handle_session_setup(&mut self, setup: SessionSetup, from: PeerId) {
/// Handle a SessionDatagram carrying a SessionSetup payload.
/// Cache coordinates from the setup message for both directions.
fn handle_session_setup(&mut self, dg: &SessionDatagram, setup: SessionSetup) {
// Cache coordinates for both directions
self.coord_cache.insert(setup.dest_addr, CacheEntry {
self.coord_cache.insert(dg.dest_addr, CacheEntry {
coords: setup.dest_coords.clone(),
expires: now() + CACHE_TTL,
});
self.coord_cache.insert(setup.src_addr, CacheEntry {
self.coord_cache.insert(dg.src_addr, CacheEntry {
coords: setup.src_coords.clone(),
expires: now() + CACHE_TTL,
});
// Forward toward destination
let next = self.greedy_next_hop(&setup.dest_coords);
self.forward(next, setup);
// Forward toward destination using find_next_hop
if let Some(next) = self.find_next_hop(&dg.dest_addr) {
self.forward(next, dg);
}
}
fn handle_data_packet(&mut self, packet: DataPacket, from: PeerId) {
/// Handle a SessionDatagram carrying a DataPacket payload.
/// Addresses come from the SessionDatagram envelope (dg.src_addr, dg.dest_addr).
fn handle_data_packet(&mut self, dg: &SessionDatagram, packet: DataPacket) {
// If packet carries coordinates, cache them
if packet.flags & COORDS_PRESENT != 0 {
if let (Some(src_coords), Some(dest_coords)) =
(&packet.src_coords, &packet.dest_coords)
{
self.coord_cache.insert(packet.dest_addr, CacheEntry {
self.coord_cache.insert(dg.dest_addr, CacheEntry {
coords: dest_coords.clone(),
expires: now() + CACHE_TTL,
});
self.coord_cache.insert(packet.src_addr, CacheEntry {
self.coord_cache.insert(dg.src_addr, CacheEntry {
coords: src_coords.clone(),
expires: now() + CACHE_TTL,
});
}
}
// Primary: bloom filter routing
let candidates = self.destination_in_filters(&packet.dest_addr);
if !candidates.is_empty() {
let next = self.select_best_candidate(&candidates);
self.forward(next, packet);
return;
}
// Fallback: greedy tree routing via cached coordinates
match self.coord_cache.get(&packet.dest_addr) {
Some(entry) => {
entry.last_used = now();
let next = self.greedy_next_hop(&entry.coords);
self.forward(next, packet);
}
// Route using find_next_hop (bloom filter → greedy tree → None)
match self.find_next_hop(&dg.dest_addr) {
Some(next) => self.forward(next, dg),
None => {
// No bloom filter hit and no cached coordinates
self.send_error(from, CoordsRequired {
dest_addr: packet.dest_addr,
// Cannot route — send error back to source via src_addr
self.send_error_to_source(dg, CoordsRequired {
dest_addr: dg.dest_addr,
reporter: self.node_addr,
});
}
}
}
/// Send an error signal back to the source of a SessionDatagram.
/// Creates a new SessionDatagram addressed to dg.src_addr.
fn send_error_to_source(&self, dg: &SessionDatagram, error: impl LinkError) {
let error_dg = SessionDatagram {
src_addr: self.node_addr, // We are the reporter
dest_addr: dg.src_addr, // Route back to original source
hop_limit: 64,
payload: error.encode(), // CoordsRequired or PathBroken
};
// Route the error — if we can't reach source either, drop silently
if let Some(next) = self.find_next_hop(&dg.src_addr) {
self.forward(next, &error_dg);
}
// If find_next_hop returns None for source: drop silently.
// No cascading errors.
}
}
```
@@ -557,7 +579,7 @@ impl Router {
```rust
struct CoordCache {
entries: HashMap<Ipv6Addr, CacheEntry>,
entries: HashMap<NodeAddr, CacheEntry>,
max_entries: usize,
}
@@ -583,22 +605,28 @@ by:
When a router's cache entry is evicted mid-session:
```text
1. Data packet arrives (minimal header), cache miss
2. Router sends CoordsRequired to packet source
3. Source marks route as cold
4. Source resends with COORDS_PRESENT flag set
5. Router caches coordinates from packet, forwards
6. After N successful packets, source clears flag
1. SessionDatagram{src:S, dest:D, payload:DataPacket} arrives, router can't route
2. Router R creates SessionDatagram{src:R, dest:S, payload:CoordsRequired{dest:D}}
3. Router routes the error back to S using find_next_hop(S)
4. S receives CoordsRequired, marks route to D as "cold"
5. S resends with COORDS_PRESENT flag set in DataPacket
6. Routers cache coordinates from DataPacket, forward normally
7. After N successful packets, S clears the flag
```
The crypto session remains active throughout—only routing state is refreshed.
From application perspective: one packet delayed, transparent recovery.
If the router also cannot route to S (no bloom filter hit, no cached
coordinates for S), the error is dropped silently. No cascading errors are
generated. The source will eventually detect the loss via application-layer
timeout.
### Sender State Machine
```rust
impl Sender {
fn send(&mut self, dest: Ipv6Addr, data: &[u8]) {
fn send(&mut self, dest: NodeAddr, data: &[u8]) {
if !self.session_established(dest) {
// Need to establish crypto session first
let dest_coords = self.discover_or_cached(dest)?;
@@ -608,9 +636,20 @@ impl Sender {
// Check route state
let include_coords = self.route_state(dest) == RouteCold;
self.send_data_packet(dest, data, include_coords);
let data_packet = DataPacket::new(data, include_coords);
// Wrap in SessionDatagram for forwarding
let dg = SessionDatagram {
src_addr: self.node_addr,
dest_addr: dest,
hop_limit: 64,
payload: data_packet.encode(),
};
self.forward_datagram(dg);
}
/// Handle CoordsRequired received inside a SessionDatagram addressed to us.
/// The SessionDatagram.src_addr identifies the reporting router (informational).
fn handle_coords_required(&mut self, err: CoordsRequired) {
// Route cache expired at intermediate router
// Crypto session still valid - just need to re-warm route
@@ -629,16 +668,21 @@ enum RouteState {
## Part 5: Packet Type Summary
All session-layer messages and error signals are carried inside a
SessionDatagram envelope (34 bytes: msg_type + src_addr + dest_addr +
hop_limit). Sizes below include the SessionDatagram header.
| Type | Purpose | Size | When Used |
|------|---------|------|-----------|
| FilterAnnounce | Bloom filter propagation | ~1 KB | Topology changes |
| LookupRequest | Discover coordinates | ~300 bytes | First contact with distant node |
| LookupResponse | Return coordinates | ~400 bytes | Reply to discovery |
| SessionSetup | Warm router caches + crypto init | ~400-700 bytes | Before data transfer |
| SessionAck | Confirm session + crypto response | ~300-500 bytes | Session confirmation |
| DataPacket | Application data | 36 bytes + payload (minimal) | Bulk of traffic |
| DataPacket | With coordinates | ~300-500 bytes + payload | After CoordsRequired |
| CoordsRequired | Request coords in next packet | ~50 bytes | Cache miss recovery |
| SessionDatagram+SessionSetup | Warm caches + crypto init | ~230-400 bytes | Before data transfer |
| SessionDatagram+SessionAck | Confirm session + crypto | ~100-200 bytes | Session confirmation |
| SessionDatagram+DataPacket | Application data | 38 bytes + payload (minimal) | Bulk of traffic |
| SessionDatagram+DataPacket | With coordinates | ~170 bytes + payload | After CoordsRequired |
| SessionDatagram+CoordsRequired | Coords needed signal | 68 bytes | Cache miss recovery |
| SessionDatagram+PathBroken | Routing failed signal | 68+ bytes | Greedy routing local minimum |
> **Note**: SessionSetup/SessionAck sizes vary based on coordinate depth and
> whether they carry crypto handshake payloads (combined establishment per
+173 -86
View File
@@ -423,26 +423,39 @@ handshakes succeed regardless of the responder's actual key parity. See
### 6.4 Handshake Integration with SessionSetup
The Noise handshake messages embed in SessionSetup/SessionAck:
The Noise handshake messages embed in SessionSetup/SessionAck, which are
carried inside a SessionDatagram envelope that provides addressing:
```text
SessionSetup {
// Routing portion (processed by routers)
src_coords: Vec<NodeAddr>,
dest_coords: Vec<NodeAddr>,
src_addr: Ipv6Addr,
dest_addr: Ipv6Addr,
SessionDatagram {
// Addressing (used by routers for forwarding and error routing)
src_addr: NodeAddr,
dest_addr: NodeAddr,
hop_limit: u8,
// Crypto portion (opaque to routers, processed by destination)
handshake_payload: Vec<u8>, // Noise IK message 1
// Payload: SessionSetup
payload: SessionSetup {
// Routing portion (processed by routers for cache warming)
src_coords: Vec<NodeAddr>,
dest_coords: Vec<NodeAddr>,
// Crypto portion (opaque to routers, processed by destination)
handshake_payload: Vec<u8>, // Noise IK message 1
}
}
SessionAck {
// Routing portion
src_coords: Vec<NodeAddr>, // Responder's coordinates
SessionDatagram {
src_addr: NodeAddr, // Responder
dest_addr: NodeAddr, // Original sender
hop_limit: u8,
// Crypto portion
handshake_payload: Vec<u8>, // Noise IK message 2
payload: SessionAck {
// Routing portion
src_coords: Vec<NodeAddr>, // Responder's coordinates
// Crypto portion
handshake_payload: Vec<u8>, // Noise IK message 2
}
}
```
@@ -529,44 +542,97 @@ for TreeAnnounce and FilterAnnounce wire formats.
## 8. Session Layer Wire Format
Session layer messages are carried inside `SessionDatagram` (type 0x40) at the
link layer. The session datagram is encrypted hop-by-hop with link keys, but
the inner payload is encrypted end-to-end with session keys.
link layer. The session datagram provides source and destination addressing for
multi-hop forwarding: it is encrypted hop-by-hop with link keys, and the inner
payload is either encrypted end-to-end with session keys (for data traffic) or
plaintext (for link-layer error signals from transit routers).
### 8.1 Message Type Codes
### 8.0 SessionDatagram Envelope
The SessionDatagram is a link-layer message (type 0x40) that carries all
routable traffic through the mesh. It provides the source and destination
addresses that transit routers use for forwarding decisions.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ SESSION DATAGRAM (0x40) │
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x40 (link-layer SessionDatagram) │
│ 1 │ src_addr │ 16 bytes │ Source node_addr │
│ 17 │ dest_addr │ 16 bytes │ Destination node_addr │
│ 33 │ hop_limit │ 1 byte │ Decremented each hop │
│ 34 │ payload │ variable │ Session-layer message (see §8.1) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Fixed header: 34 bytes
```
**src_addr**: The originator of the datagram. For data traffic, this is the
source endpoint. For error signals (CoordsRequired, PathBroken), this is the
transit router that generated the error. Transit routers use `src_addr` to
route error signals back to the packet's originator.
**dest_addr**: The intended recipient. Transit routers use this for next-hop
selection via `find_next_hop()`.
**hop_limit**: Decremented at each forwarding hop. When it reaches zero, the
datagram is dropped silently. This prevents infinite forwarding loops. Initial
value is configurable (default 64).
### 8.1 Payload Message Types
The SessionDatagram payload begins with a message type byte, followed by the
message-specific content.
**Session-layer messages** (end-to-end encrypted with session keys):
| Type Code | Message | Direction | Purpose |
|-----------|----------------|-----------|-----------------------------------|
| 0x00 | SessionSetup | S → D | Establish session + warm caches |
| 0x01 | SessionAck | D → S | Confirm session establishment |
| 0x10 | DataPacket | Both | Application data |
**Link-layer error signals** (plaintext, generated by transit routers):
| Type Code | Message | Direction | Purpose |
|-----------|----------------|-----------|-----------------------------------|
| 0x20 | CoordsRequired | R → S | Router cache miss |
| 0x21 | PathBroken | R → S | Greedy routing failed |
> **Address terminology**: The `src_addr` and `dest_addr` fields in session packet
> headers are node_addrs (16-byte truncated SHA-256 hashes of pubkeys). These are visible to
> intermediate routers for routing decisions. The actual FIPS addresses (pubkeys/npubs)
> are exchanged only during the Noise IK handshake and never appear in packet
> headers—routers cannot determine endpoint identities from the node_addrs they see.
Error signals are generated by transit router R and sent back to the source S
inside a *new* SessionDatagram with `src_addr=R, dest_addr=S`. They are
plaintext (not end-to-end encrypted) because the transit router has no session
with the source. Link-layer encryption protects them hop-by-hop.
> **Address terminology**: The `src_addr` and `dest_addr` fields in the
> SessionDatagram header are node_addrs (16-byte truncated SHA-256 hashes of
> pubkeys). These are visible to intermediate routers for routing decisions. The
> actual FIPS addresses (pubkeys/npubs) are exchanged only during the Noise IK
> handshake and never appear in packet headers—routers cannot determine endpoint
> identities from the node_addrs they see.
### 8.2 SessionSetup (0x00)
Establishes a crypto session and warms router coordinate caches along the path.
The `src_addr` and `dest_addr` are carried in the enclosing SessionDatagram
envelope (§8.0); the SessionSetup payload carries only coordinates and the
Noise handshake.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
SESSION SETUP PACKET
│ SESSION SETUP PAYLOAD (inside SessionDatagram)
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x00 │
│ 1 │ flags │ 1 byte │ Bit 0: REQUEST_ACK │
│ │ │ │ Bit 1: BIDIRECTIONAL │
│ 2 │ src_addr 16 bytes │ Source node_addr
18dest_addr │ 16 bytes │ Destination node_addr
│ 34 │ src_coords_count │ 2 bytes │ u16 LE, number of src coord entries │
│ 36 │ src_coords │ 16 × n │ NodeAddr array (self → root) │
│ 2 │ src_coords_count2 bytes u16 LE, number of src coord entries
4src_coords │ 16 × n │ NodeAddr array (self → root)
│ ... │ dest_coords_count│ 2 bytes │ u16 LE, number of dest coord entries│
│ ... │ dest_coords │ 16 × m │ NodeAddr array (dest → root)
│ ... │ dest_coords │ 16 × m │ NodeAddr array (dest → root) │
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg1 (82 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
@@ -575,33 +641,32 @@ Establishes a crypto session and warms router coordinate caches along the path.
**Example** (depth 3 source, depth 4 destination, with Noise handshake):
```text
┌──────┬───────┬──────────────────┬──────────────────┬───────┬─────────────┐
│ 0x00 │ 0x01 │ src_addr │ dest_addr │ 0x03 │ src_coords │
type │ flags │ 16 bytes │ 16 bytes │ count │ 3 × 16 bytes
├──────┴───────┴──────────────────┴──────────────────┴───────┴─────────────┤
│ 0x04 │ dest_coords │ 0x52 │ handshake_payload │
│ count │ 4 × 16 bytes │ len=82│ 82 bytes (Noise IK msg1) │
└───────┴───────────────┴───────┴──────────────────────────────────────────┘
SessionDatagram { src: S, dest: D, hop_limit: 64 } +
┌──────┬───────┬───────┬─────────────┬───────┬───────────────┬───────┬───────┐
0x00 │ 0x01 │ 0x03 │ src_coords │ 0x04 │ dest_coords │ 0x52 │ noise
│ type │ flags │ count │ 3 × 16 bytes│ count │ 4 × 16 bytes │ len=82│ msg1 │
└──────┴───────┴───────┴─────────────┴───────┴───────────────┴───────┴───────┘
Total: 1 + 1 + 16 + 16 + 2 + 48 + 2 + 64 + 2 + 82 = 234 bytes
SessionDatagram header: 34 bytes
SessionSetup payload: 1 + 1 + 2 + 48 + 2 + 64 + 2 + 82 = 202 bytes
Total: 236 bytes
```
### 8.3 SessionAck (0x01)
Confirms session establishment and completes the Noise handshake.
Confirms session establishment and completes the Noise handshake. Addressing
is in the enclosing SessionDatagram envelope.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
SESSION ACK PACKET
│ SESSION ACK PAYLOAD (inside SessionDatagram)
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x01 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ src_addr │ 16 bytes │ Acknowledger's node_addr
18dest_addr │ 16 bytesOriginal sender's node_addr
│ 34 │ src_coords_count │ 2 bytes │ u16 LE │
│ 36 │ src_coords │ 16 × n │ Acknowledger's coords (for caching) │
│ 2 │ src_coords_count │ 2 bytesu16 LE
4src_coords │ 16 × n Acknowledger's coords (for caching)
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg2 (33 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
@@ -609,79 +674,90 @@ Confirms session establishment and completes the Noise handshake.
### 8.4 DataPacket (0x10)
Carries encrypted application data (typically IPv6 payloads).
Carries encrypted application data (typically IPv6 payloads). Addressing and
hop limit are in the enclosing SessionDatagram envelope.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
DATA PACKET (Minimal Header)
│ DATA PACKET (Minimal Header, inside SessionDatagram)
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x10 │
│ 1 │ flags │ 1 byte │ Bit 0: COORDS_PRESENT │
│ 2 │ hop_limit │ 1 byte │ Decremented each hop
3reserved │ 1 byte │ Alignment padding
│ 4 │ payload_length │ 2 bytes │ u16 LE │
│ 6 │ src_addr │ 16 bytes │ Source node_addr │
│ 22 │ dest_addr │ 16 bytes │ Destination node_addr │
│ 38 │ payload │ variable │ Encrypted application data │
│ 2 │ payload_length │ 2 bytes │ u16 LE
4payload │ variable │ Encrypted application data
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Minimal header: 38 bytes
Minimal header: 4 bytes (+ 34 SessionDatagram header = 38 bytes total)
```
When `COORDS_PRESENT` flag is set (route warming after CoordsRequired):
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA PACKET (With Coordinates)
│ DATA PACKET (With Coordinates, inside SessionDatagram)
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x10 │
│ 1 │ flags │ 1 byte │ 0x01 (COORDS_PRESENT) │
│ 2 │ hop_limit │ 1 byte │ Decremented each hop
3reserved │ 1 byte │ Alignment padding
4payload_length │ 2 bytes u16 LE
│ 6 │ src_addr │ 16 bytes │ Source node_addr │
│ 22 │ dest_addr │ 16 bytes │ Destination node_addr │
│ 38 │ src_coords_count │ 2 bytes │ u16 LE │
│ 40 │ src_coords │ 16 × n │ Source coordinates │
│ 2 │ payload_length │ 2 bytes │ u16 LE
4src_coords_count │ 2 bytes │ u16 LE
6src_coords │ 16 × n │ Source coordinates
│ ... │ dest_coords_count│ 2 bytes │ u16 LE │
│ ... │ dest_coords │ 16 × m │ Destination coordinates │
│ ... │ payload │ variable │ Encrypted application data │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
With depth-4 coords both directions: 38 + 2 + 64 + 2 + 64 = 170 bytes header
With depth-4 coords both directions:
DataPacket: 4 + 2 + 64 + 2 + 64 = 136 bytes header
+ SessionDatagram: 34 bytes
Total: 170 bytes header
```
### 8.5 CoordsRequired (0x20)
Sent by an intermediate router when it cannot forward a DataPacket due to
coordinate cache miss.
Sent by an intermediate router when it cannot forward a SessionDatagram due to
coordinate cache miss for the destination. This is a **link-layer error signal**:
the transit router generates a new SessionDatagram addressed back to the
original source, with the CoordsRequired payload in plaintext (not end-to-end
encrypted, since the transit router has no session with the source).
**Error routing**: Router R receives a SessionDatagram `{src: S, dest: D}` but
cannot route to D. R creates a new SessionDatagram `{src: R, dest: S}` carrying
a CoordsRequired payload, and calls `find_next_hop(S)` to route it back to S.
If R also cannot route to S, the error is dropped silently (no cascading errors).
**CoordsRequired payload** (inside SessionDatagram):
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ COORDS REQUIRED PACKET
│ COORDS REQUIRED PAYLOAD
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x20 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ dest_addr │ 16 bytes │ The node_addr we couldn't route
│ 2 │ dest_addr │ 16 bytes │ The node_addr we couldn't route to
│ 18 │ reporter │ 16 bytes │ NodeAddr of reporting router │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Total: 34 bytes
Payload: 34 bytes
Wrapped in SessionDatagram: 34 + 34 = 68 bytes total
```
### 8.6 PathBroken (0x21)
Sent when greedy routing fails (no peer is closer to destination).
Sent when greedy routing fails (no peer is closer to destination). Like
CoordsRequired, this is a **link-layer error signal** carried inside a new
SessionDatagram addressed back to the original source.
**PathBroken payload** (inside SessionDatagram):
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ PATH BROKEN PACKET
│ PATH BROKEN PAYLOAD
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
@@ -714,34 +790,45 @@ A DataPacket from source S to destination D, transiting router R:
│ │ Decrypt with S↔R link keys │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ LINK MESSAGE (plaintext for R) │ │
│ ├───────────┬───────────────────────────────────────────────────────────┤ │
│ │ 0x40 │ SessionDatagram payload │ │
│ │ msg_type │ (routable by R, encrypted end-to-end) │ │
│ └───────────┴───────────────────────────────────────────────────────────┘ │
│ │ SESSION DATAGRAM (0x40) — plaintext for R │ │
│ ├───────────┬────────────────────────────────────────────────────────┤ │
│ │ 0x40 │ src_addr │ dest_addr │ hop_limit│ payload │ │
│ │ msg_type │ S (16 bytes) │ D (16 bytes) │ 64 │ (session msg) │ │
│ └───────────┴────────────────────────────────────────────────────────┘ │
│ │ │
│ │ R reads src_addr + dest_addr for routing │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ SESSION LAYER (S↔D encrypted) │ │
│ │ SESSION LAYER PAYLOAD (S↔D encrypted) │ │
│ ├───────────┬───────┬──────────┬──────────┬─────────────────────────────┤ │
│ │ 0x10 │ flags │ hop_limit│ pay_len │ src_addr │ dest_addr │ │
│ │ DataPacket│ 0x00 │ 64 │ 1400 │ 16 bytes │ 16 bytes │ │
───────────┴───────┴──────────┴──────────┴────────────────────────────
│ │ │ │
│ │ ENCRYPTED PAYLOAD (S↔D session keys) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ IPv6 packet or application data │ │ │
│ │ │ (+ 16-byte AEAD tag) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │ 0x10 │ flags │ reserved │ pay_len │ encrypted application data │ │
│ │ DataPacket│ 0x00 │ 0x00 │ 1400 │ (S↔D session keys + tag) │ │
───────────┴───────┴──────────┴──────────┴────────────────────────────
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Router R can see: dest_addr (for routing decision)
Router R cannot see: payload contents (encrypted with S↔D keys)
Router R can see: src_addr, dest_addr, hop_limit (for routing/error signaling)
Router R cannot see: payload contents (encrypted with S↔D session keys)
```
### 8.7.1 Error Routing Example
When router R cannot forward a SessionDatagram from S to D:
```text
1. R receives SessionDatagram { src: S, dest: D, payload: DataPacket{...} }
2. R has no cached coordinates for D and no bloom filter hit
3. R creates NEW SessionDatagram:
{ src: R, dest: S, hop_limit: 64,
payload: CoordsRequired { dest: D, reporter: R } }
4. R calls find_next_hop(S) to route the error back
5. If R can also not route to S: drop silently (no cascading errors)
```
This avoids the need for transit routers to have end-to-end sessions with
the source. The `src_addr` field in the original SessionDatagram tells the
transit router where to send the error.
### 8.8 Encoding Rules
- All multi-byte integers are **little-endian**
+6 -1
View File
@@ -892,11 +892,16 @@ Post-handshake data packets between authenticated peers.
│ │ 1 │ payload │ variable │ Message-specific payload │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Link message types:
│ Link message types (inside encrypted frame):
│ 0x10 = TreeAnnounce 0x30 = LookupRequest │
│ 0x20 = FilterAnnounce 0x31 = LookupResponse │
│ 0x40 = SessionDatagram 0x50 = Disconnect │
│ │
│ SessionDatagram (0x40) carries session-layer payloads: │
│ 0x00 = SessionSetup 0x10 = DataPacket │
│ 0x01 = SessionAck 0x20 = CoordsRequired │
│ 0x21 = PathBroken │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```