mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +00:00
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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 │
|
||||
│ 18 │ dest_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_count │ 2 bytes │ u16 LE, number of src coord entries │
|
||||
│ 4 │ src_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 │
|
||||
│ 18 │ dest_addr │ 16 bytes │ Original 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 bytes │ u16 LE │
|
||||
│ 4 │ src_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 │
|
||||
│ 3 │ reserved │ 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 │
|
||||
│ 4 │ payload │ 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 │
|
||||
│ 3 │ reserved │ 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 │ src_coords_count │ 2 bytes │ u16 LE │
|
||||
│ 40 │ src_coords │ 16 × n │ Source coordinates │
|
||||
│ 2 │ payload_length │ 2 bytes │ u16 LE │
|
||||
│ 4 │ src_coords_count │ 2 bytes │ u16 LE │
|
||||
│ 6 │ src_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**
|
||||
|
||||
@@ -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 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
+28
-39
@@ -713,16 +713,19 @@ impl Node {
|
||||
/// Routing priority:
|
||||
/// 1. Destination is self → `None` (local delivery)
|
||||
/// 2. Destination is a direct peer → that peer
|
||||
/// 3. Bloom filter candidates + greedy tree routing → among peers whose
|
||||
/// 3. Bloom filter candidates with cached dest coords → among peers whose
|
||||
/// bloom filter contains the destination, pick the one that minimizes
|
||||
/// tree distance to the destination (if dest coords are cached), with
|
||||
/// tree distance to the destination, with
|
||||
/// `(link_cost, tree_distance_to_dest, node_addr)` tie-breaking.
|
||||
/// Falls back to greedy tree routing if no bloom filter hits.
|
||||
/// 4. No route → `None`
|
||||
/// The self-distance check ensures only peers strictly closer to the
|
||||
/// destination than us are considered (prevents routing loops).
|
||||
/// 4. Greedy tree routing fallback (requires cached dest coords)
|
||||
/// 5. No route → `None`
|
||||
///
|
||||
/// The self-distance check from greedy routing also applies to bloom
|
||||
/// filter candidates: a peer is only selected if it is strictly closer
|
||||
/// to the destination than we are (prevents routing loops).
|
||||
/// Both the bloom filter and tree routing paths require cached destination
|
||||
/// coordinates. Without coordinates, the node cannot make loop-free
|
||||
/// forwarding decisions. The caller should signal `CoordsRequired` back
|
||||
/// to the source when `None` is returned for a non-local destination.
|
||||
pub fn find_next_hop(&self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
|
||||
// 1. Local delivery
|
||||
if dest_node_addr == self.node_addr() {
|
||||
@@ -736,21 +739,20 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
// Look up destination coords (used by both bloom and tree paths)
|
||||
// Look up destination coords (required by both bloom and tree paths)
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let dest_coords = self.coord_cache.get(dest_node_addr, now_ms);
|
||||
let dest_coords = self.coord_cache.get(dest_node_addr, now_ms)?;
|
||||
|
||||
// 3. Bloom filter candidates, scored by tree distance to dest
|
||||
// 3. Bloom filter candidates — requires dest_coords for loop-free selection
|
||||
let candidates: Vec<&ActivePeer> = self.destination_in_filters(dest_node_addr);
|
||||
if !candidates.is_empty() {
|
||||
return self.select_best_candidate(&candidates, dest_coords);
|
||||
}
|
||||
|
||||
// 4. Greedy tree routing fallback
|
||||
let dest_coords = dest_coords?;
|
||||
let next_hop_id = self.tree_state.find_next_hop(dest_coords)?;
|
||||
|
||||
self.peers.get(&next_hop_id).filter(|p| p.can_send())
|
||||
@@ -758,22 +760,18 @@ impl Node {
|
||||
|
||||
/// Select the best peer from a set of bloom filter candidates.
|
||||
///
|
||||
/// When dest_coords are available, uses distance from each candidate's
|
||||
/// coordinates to the destination as the primary metric (after link_cost).
|
||||
/// Only selects peers that are strictly closer to the destination than
|
||||
/// we are (self-distance check prevents loops).
|
||||
///
|
||||
/// When dest_coords are not available, falls back to distance from us
|
||||
/// to the candidate peer (a weaker heuristic — prefers closer peers on
|
||||
/// the theory that shorter paths are better).
|
||||
/// Uses distance from each candidate's tree coordinates to the destination
|
||||
/// as the primary metric (after link_cost). Only selects peers that are
|
||||
/// strictly closer to the destination than we are (self-distance check
|
||||
/// prevents routing loops).
|
||||
///
|
||||
/// Ordering: `(link_cost, distance_to_dest, node_addr)`.
|
||||
fn select_best_candidate<'a>(
|
||||
&'a self,
|
||||
candidates: &[&'a ActivePeer],
|
||||
dest_coords: Option<&crate::tree::TreeCoordinate>,
|
||||
dest_coords: &crate::tree::TreeCoordinate,
|
||||
) -> Option<&'a ActivePeer> {
|
||||
let my_distance = dest_coords.map(|dc| self.tree_state.my_coords().distance_to(dc));
|
||||
let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
|
||||
|
||||
let mut best: Option<(&ActivePeer, f64, usize)> = None;
|
||||
|
||||
@@ -784,25 +782,16 @@ impl Node {
|
||||
|
||||
let cost = candidate.link_cost();
|
||||
|
||||
// Compute distance: peer→dest if coords available, else us→peer
|
||||
let dist = match dest_coords {
|
||||
Some(dc) => self
|
||||
.tree_state
|
||||
.peer_coords(candidate.node_addr())
|
||||
.map(|pc| pc.distance_to(dc))
|
||||
.unwrap_or(usize::MAX),
|
||||
None => self
|
||||
.tree_state
|
||||
.distance_to_peer(candidate.node_addr())
|
||||
.unwrap_or(usize::MAX),
|
||||
};
|
||||
let dist = self
|
||||
.tree_state
|
||||
.peer_coords(candidate.node_addr())
|
||||
.map(|pc| pc.distance_to(dest_coords))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
// Self-distance check: when dest coords are available,
|
||||
// only consider peers that are strictly closer than us
|
||||
if let Some(my_dist) = my_distance {
|
||||
if dist >= my_dist {
|
||||
continue;
|
||||
}
|
||||
// Self-distance check: only consider peers strictly closer
|
||||
// to the destination than we are (prevents routing loops)
|
||||
if dist >= my_distance {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dominated = match &best {
|
||||
|
||||
+101
-12
@@ -54,6 +54,7 @@ fn test_routing_unknown_destination() {
|
||||
fn test_routing_bloom_filter_hit() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let my_addr = *node.node_addr();
|
||||
|
||||
// Create two peers
|
||||
let link_id1 = LinkId::new(1);
|
||||
@@ -68,8 +69,27 @@ fn test_routing_bloom_filter_hit() {
|
||||
node.add_connection(conn2).unwrap();
|
||||
node.promote_connection(link_id2, id2, 2000).unwrap();
|
||||
|
||||
// Destination not directly connected
|
||||
// Set up tree: we are root, both peers are our children
|
||||
let peer1_coords = TreeCoordinate::from_addrs(vec![peer1_addr, my_addr]).unwrap();
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(peer1_addr, my_addr, 1, 1000),
|
||||
peer1_coords,
|
||||
);
|
||||
let peer2_coords = TreeCoordinate::from_addrs(vec![peer2_addr, my_addr]).unwrap();
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(peer2_addr, my_addr, 1, 1000),
|
||||
peer2_coords,
|
||||
);
|
||||
|
||||
// Destination not directly connected — placed under peer1 in the tree
|
||||
let dest = make_node_addr(99);
|
||||
let dest_coords =
|
||||
TreeCoordinate::from_addrs(vec![dest, peer1_addr, my_addr]).unwrap();
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
node.coord_cache_mut().insert(dest, dest_coords, now_ms);
|
||||
|
||||
// Add dest to peer1's bloom filter only
|
||||
let peer1 = node.get_peer_mut(&peer1_addr).unwrap();
|
||||
@@ -77,7 +97,7 @@ fn test_routing_bloom_filter_hit() {
|
||||
filter.insert(&dest);
|
||||
peer1.update_filter(filter, 1, 3000);
|
||||
|
||||
// Should route through peer1 (bloom filter hit)
|
||||
// Should route through peer1 (bloom filter hit, closer to dest)
|
||||
let result = node.find_next_hop(&dest);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap().node_addr(), &peer1_addr);
|
||||
@@ -90,6 +110,7 @@ fn test_routing_bloom_filter_hit() {
|
||||
fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let my_addr = *node.node_addr();
|
||||
|
||||
// Create three peers
|
||||
let mut peer_addrs = Vec::new();
|
||||
@@ -102,7 +123,25 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
}
|
||||
|
||||
// Set up tree: we are root, all peers are our children (equidistant)
|
||||
for &addr in &peer_addrs {
|
||||
let coords = TreeCoordinate::from_addrs(vec![addr, my_addr]).unwrap();
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(addr, my_addr, 1, 1000),
|
||||
coords,
|
||||
);
|
||||
}
|
||||
|
||||
// Destination placed under the first peer (arbitrary — all peers are
|
||||
// equidistant from dest since dest is 2 hops from root via any child)
|
||||
let dest = make_node_addr(99);
|
||||
let dest_coords =
|
||||
TreeCoordinate::from_addrs(vec![dest, peer_addrs[0], my_addr]).unwrap();
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
node.coord_cache_mut().insert(dest, dest_coords, now_ms);
|
||||
|
||||
// Add dest to ALL peers' bloom filters
|
||||
for &addr in &peer_addrs {
|
||||
@@ -112,13 +151,13 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
peer.update_filter(filter, 1, 3000);
|
||||
}
|
||||
|
||||
// All peers have equal link_cost (1.0) and no tree coords set,
|
||||
// so tree distance is usize::MAX for all. Tie-break by smallest node_addr.
|
||||
// All peers have equal link_cost (1.0). peer_addrs[0] is closest to dest
|
||||
// (distance 1 vs distance 3 for the others). Self-distance check filters
|
||||
// peers that aren't strictly closer than us (our distance = 2).
|
||||
// peer_addrs[0] has distance 1 (passes), others have distance 3 (filtered).
|
||||
let result = node.find_next_hop(&dest);
|
||||
assert!(result.is_some());
|
||||
|
||||
let smallest_addr = peer_addrs.iter().min().unwrap();
|
||||
assert_eq!(result.unwrap().node_addr(), smallest_addr);
|
||||
assert_eq!(result.unwrap().node_addr(), &peer_addrs[0]);
|
||||
}
|
||||
|
||||
// === Greedy tree routing ===
|
||||
@@ -183,6 +222,42 @@ fn test_routing_tree_no_coords_in_cache() {
|
||||
assert!(node.find_next_hop(&dest).is_none());
|
||||
}
|
||||
|
||||
// === Bloom filter without coords → no route (loop prevention) ===
|
||||
|
||||
#[test]
|
||||
fn test_routing_bloom_hit_without_coords_returns_none() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
// Create two peers
|
||||
let link_id1 = LinkId::new(1);
|
||||
let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let peer1_addr = *id1.node_addr();
|
||||
node.add_connection(conn1).unwrap();
|
||||
node.promote_connection(link_id1, id1, 2000).unwrap();
|
||||
|
||||
let link_id2 = LinkId::new(2);
|
||||
let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
let peer2_addr = *id2.node_addr();
|
||||
node.add_connection(conn2).unwrap();
|
||||
node.promote_connection(link_id2, id2, 2000).unwrap();
|
||||
|
||||
let dest = make_node_addr(99);
|
||||
|
||||
// Add dest to BOTH peers' bloom filters
|
||||
for &addr in &[peer1_addr, peer2_addr] {
|
||||
let peer = node.get_peer_mut(&addr).unwrap();
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&dest);
|
||||
peer.update_filter(filter, 1, 3000);
|
||||
}
|
||||
|
||||
// Bloom filter candidates exist, but dest coords are NOT cached.
|
||||
// find_next_hop must return None to prevent routing loops.
|
||||
// The caller should signal CoordsRequired back to the source.
|
||||
assert!(node.find_next_hop(&dest).is_none());
|
||||
}
|
||||
|
||||
// === Integration: converged network ===
|
||||
|
||||
#[tokio::test]
|
||||
@@ -270,18 +345,31 @@ async fn test_routing_bloom_preferred_over_tree() {
|
||||
|
||||
drain_all_packets(&mut nodes, false).await;
|
||||
|
||||
// Create a destination beyond the network
|
||||
// Create a destination beyond the network and cache its coords.
|
||||
// Place dest as a child of peer2 in the converged tree so bloom
|
||||
// filter routing selects peer2 (strictly closer to dest than us).
|
||||
let dest = make_node_addr(99);
|
||||
let peer2_addr = *nodes[2].node.node_addr();
|
||||
let mut dest_path: Vec<NodeAddr> =
|
||||
nodes[2].node.tree_state().my_coords().node_addrs().copied().collect();
|
||||
dest_path.insert(0, dest);
|
||||
let dest_coords = TreeCoordinate::from_addrs(dest_path).unwrap();
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
nodes[0]
|
||||
.node
|
||||
.coord_cache_mut()
|
||||
.insert(dest, dest_coords, now_ms);
|
||||
|
||||
// Add dest to peer 2's bloom filter (from node 0's perspective)
|
||||
let peer2_addr = *nodes[2].node.node_addr();
|
||||
let peer2 = nodes[0].node.get_peer_mut(&peer2_addr).unwrap();
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&dest);
|
||||
peer2.update_filter(filter, 100, 50000);
|
||||
|
||||
// Even though we could use tree routing (if coords were cached),
|
||||
// the bloom filter hit should be preferred.
|
||||
// Bloom filter hit with cached coords should route via peer 2.
|
||||
let hop = nodes[0].node.find_next_hop(&dest);
|
||||
assert!(hop.is_some(), "Should route via bloom filter");
|
||||
assert_eq!(
|
||||
@@ -401,7 +489,8 @@ async fn test_routing_reachability_100_nodes() {
|
||||
|
||||
// Populate coord caches: every node learns every other node's coordinates.
|
||||
// In production this happens via SessionSetup/LookupResponse; here we
|
||||
// inject them directly so routing can make progress-based decisions.
|
||||
// inject them directly. Bloom filter routing requires cached dest_coords
|
||||
// for loop-free forwarding — without coords, find_next_hop returns None.
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
|
||||
+27
-7
@@ -244,25 +244,45 @@ impl Disconnect {
|
||||
// Session Datagram (Link-Layer Encapsulation)
|
||||
// ============================================================================
|
||||
|
||||
/// Encapsulated session-layer datagram for forwarding.
|
||||
/// Encapsulated session-layer datagram for multi-hop forwarding.
|
||||
///
|
||||
/// This is a link-layer message that carries an opaque, end-to-end encrypted
|
||||
/// session-layer payload. Intermediate nodes route based on the destination
|
||||
/// address but cannot decrypt the payload.
|
||||
/// This is a link-layer message (type 0x40) that carries session-layer
|
||||
/// payloads through the mesh. The envelope provides source and destination
|
||||
/// addressing that transit routers use for forwarding decisions and error
|
||||
/// routing.
|
||||
///
|
||||
/// ## Wire Format (34-byte fixed header)
|
||||
///
|
||||
/// | Offset | Field | Size | Description |
|
||||
/// |--------|-----------|----------|--------------------------------|
|
||||
/// | 0 | msg_type | 1 byte | 0x40 |
|
||||
/// | 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 |
|
||||
///
|
||||
/// The payload is either end-to-end encrypted (SessionSetup, SessionAck,
|
||||
/// DataPacket) or plaintext link-layer error signals (CoordsRequired,
|
||||
/// PathBroken) generated by transit routers.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionDatagram {
|
||||
/// Source node address (originator of this datagram).
|
||||
/// For data traffic: the source endpoint.
|
||||
/// For error signals: the transit router that generated the error.
|
||||
pub src_addr: NodeAddr,
|
||||
/// Destination node address (for routing decisions).
|
||||
pub dest_addr: NodeAddr,
|
||||
/// Hop limit (decremented at each hop).
|
||||
/// Hop limit (decremented at each hop, dropped at zero).
|
||||
pub hop_limit: u8,
|
||||
/// Encrypted session-layer payload (opaque to intermediate nodes).
|
||||
/// Session-layer payload (e2e encrypted or plaintext error signal).
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SessionDatagram {
|
||||
/// Create a new session datagram.
|
||||
pub fn new(dest_addr: NodeAddr, payload: Vec<u8>) -> Self {
|
||||
pub fn new(src_addr: NodeAddr, dest_addr: NodeAddr, payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
src_addr,
|
||||
dest_addr,
|
||||
hop_limit: 64,
|
||||
payload,
|
||||
|
||||
+54
-115
@@ -8,11 +8,13 @@ use std::fmt;
|
||||
// Session Layer Message Types
|
||||
// ============================================================================
|
||||
|
||||
/// Session-layer message type identifiers.
|
||||
/// SessionDatagram payload message type identifiers.
|
||||
///
|
||||
/// These messages are exchanged end-to-end between FIPS nodes, encrypted
|
||||
/// with session keys that intermediate nodes cannot read. They are carried
|
||||
/// as payloads inside `LinkMessageType::SessionDatagram`.
|
||||
/// These messages are carried as payloads inside `SessionDatagram` (link
|
||||
/// message type 0x40). Session-layer messages (SessionSetup, SessionAck,
|
||||
/// DataPacket) are end-to-end encrypted with session keys. Error signals
|
||||
/// (CoordsRequired, PathBroken) are plaintext link-layer messages generated
|
||||
/// by transit routers that cannot establish e2e sessions with the source.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum SessionMessageType {
|
||||
@@ -26,10 +28,10 @@ pub enum SessionMessageType {
|
||||
/// Encrypted IPv6 datagram payload.
|
||||
DataPacket = 0x10,
|
||||
|
||||
// Errors (0x20-0x2F)
|
||||
/// Router cache miss - needs coordinates.
|
||||
// Link-layer error signals (0x20-0x2F) — plaintext, from transit routers
|
||||
/// Router cache miss — needs coordinates (link-layer error signal).
|
||||
CoordsRequired = 0x20,
|
||||
/// Routing failure (local minimum or unreachable).
|
||||
/// Routing failure — local minimum or unreachable (link-layer error signal).
|
||||
PathBroken = 0x21,
|
||||
}
|
||||
|
||||
@@ -123,14 +125,12 @@ impl SessionFlags {
|
||||
|
||||
/// Session setup to establish cached coordinate state.
|
||||
///
|
||||
/// Sent before data packets to warm router caches with coordinate
|
||||
/// information. Routers along the path cache the mappings.
|
||||
/// Carried inside a SessionDatagram envelope which provides src_addr and
|
||||
/// dest_addr. The SessionSetup payload contains only coordinates and the
|
||||
/// Noise handshake data needed for route cache warming and session
|
||||
/// establishment.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionSetup {
|
||||
/// Source node address.
|
||||
pub src_addr: NodeAddr,
|
||||
/// Destination node address.
|
||||
pub dest_addr: NodeAddr,
|
||||
/// Source coordinates (for return path caching).
|
||||
pub src_coords: TreeCoordinate,
|
||||
/// Destination coordinates (for forward routing).
|
||||
@@ -141,15 +141,8 @@ pub struct SessionSetup {
|
||||
|
||||
impl SessionSetup {
|
||||
/// Create a new session setup message.
|
||||
pub fn new(
|
||||
src_addr: NodeAddr,
|
||||
dest_addr: NodeAddr,
|
||||
src_coords: TreeCoordinate,
|
||||
dest_coords: TreeCoordinate,
|
||||
) -> Self {
|
||||
pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
|
||||
Self {
|
||||
src_addr,
|
||||
dest_addr,
|
||||
src_coords,
|
||||
dest_coords,
|
||||
flags: SessionFlags::new(),
|
||||
@@ -169,25 +162,19 @@ impl SessionSetup {
|
||||
|
||||
/// Session acknowledgement.
|
||||
///
|
||||
/// Sent in response to SessionSetup when request_ack is set.
|
||||
/// Carried inside a SessionDatagram envelope which provides src_addr and
|
||||
/// dest_addr. The SessionAck payload contains the acknowledger's coordinates
|
||||
/// for route cache warming.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionAck {
|
||||
/// Source node address (the acknowledger).
|
||||
pub src_addr: NodeAddr,
|
||||
/// Destination node address (original session initiator).
|
||||
pub dest_addr: NodeAddr,
|
||||
/// Acknowledger's coordinates.
|
||||
pub src_coords: TreeCoordinate,
|
||||
}
|
||||
|
||||
impl SessionAck {
|
||||
/// Create a new session acknowledgement.
|
||||
pub fn new(src_addr: NodeAddr, dest_addr: NodeAddr, src_coords: TreeCoordinate) -> Self {
|
||||
Self {
|
||||
src_addr,
|
||||
dest_addr,
|
||||
src_coords,
|
||||
}
|
||||
pub fn new(src_coords: TreeCoordinate) -> Self {
|
||||
Self { src_coords }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,73 +239,44 @@ impl DataFlags {
|
||||
}
|
||||
}
|
||||
|
||||
/// Data packet header size in bytes (excluding payload).
|
||||
/// flags(1) + hop_limit(1) + payload_length(2) + src_addr(16) + dest_addr(16) = 36
|
||||
pub const DATA_HEADER_SIZE: usize = 36;
|
||||
/// DataPacket header size in bytes (excluding payload).
|
||||
/// msg_type(1) + flags(1) + payload_length(2) = 4
|
||||
/// (Addressing and hop_limit are in the SessionDatagram envelope.)
|
||||
pub const DATA_HEADER_SIZE: usize = 4;
|
||||
|
||||
/// Minimal data packet with addresses only (no coordinates).
|
||||
/// Encrypted application data carried inside a SessionDatagram.
|
||||
///
|
||||
/// The 36-byte header contains:
|
||||
/// - flags (1 byte)
|
||||
/// - hop_limit (1 byte)
|
||||
/// The 4-byte header contains:
|
||||
/// - msg_type (1 byte): 0x10
|
||||
/// - flags (1 byte): COORDS_PRESENT, etc.
|
||||
/// - payload_length (2 bytes)
|
||||
/// - src_addr (16 bytes)
|
||||
/// - dest_addr (16 bytes)
|
||||
///
|
||||
/// Routers use cached coordinates for routing decisions.
|
||||
/// Addressing (src_addr, dest_addr) and hop_limit are provided by the
|
||||
/// enclosing SessionDatagram envelope. The total on-wire overhead for a
|
||||
/// minimal data packet is 34 (SessionDatagram) + 4 (DataPacket) = 38 bytes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DataPacket {
|
||||
/// Packet flags.
|
||||
pub flags: DataFlags,
|
||||
/// Hop limit (TTL).
|
||||
pub hop_limit: u8,
|
||||
/// Source node address.
|
||||
pub src_addr: NodeAddr,
|
||||
/// Destination node address.
|
||||
pub dest_addr: NodeAddr,
|
||||
/// Payload data.
|
||||
/// Payload data (end-to-end encrypted application data).
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DataPacket {
|
||||
/// Create a new data packet.
|
||||
pub fn new(src_addr: NodeAddr, dest_addr: NodeAddr, payload: Vec<u8>) -> Self {
|
||||
pub fn new(payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
flags: DataFlags::new(),
|
||||
hop_limit: 64,
|
||||
src_addr,
|
||||
dest_addr,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the hop limit.
|
||||
pub fn with_hop_limit(mut self, hop_limit: u8) -> Self {
|
||||
self.hop_limit = hop_limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the flags.
|
||||
pub fn with_flags(mut self, flags: DataFlags) -> Self {
|
||||
self.flags = flags;
|
||||
self
|
||||
}
|
||||
|
||||
/// Decrement hop limit, returning false if exhausted.
|
||||
pub fn decrement_hop_limit(&mut self) -> bool {
|
||||
if self.hop_limit > 0 {
|
||||
self.hop_limit -= 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the packet can be forwarded.
|
||||
pub fn can_forward(&self) -> bool {
|
||||
self.hop_limit > 0
|
||||
}
|
||||
|
||||
/// Get the payload length.
|
||||
pub fn payload_len(&self) -> usize {
|
||||
self.payload.len()
|
||||
@@ -339,10 +297,14 @@ impl DataPacket {
|
||||
// Error Messages
|
||||
// ============================================================================
|
||||
|
||||
/// Error indicating router cache miss - needs coordinates.
|
||||
/// Link-layer error signal indicating router cache miss.
|
||||
///
|
||||
/// Sent back to the source when a router doesn't have cached
|
||||
/// coordinates for the destination.
|
||||
/// Generated by a transit router when it cannot forward a SessionDatagram
|
||||
/// due to missing cached coordinates for the destination. Carried inside
|
||||
/// a new SessionDatagram addressed back to the original source
|
||||
/// (src_addr=reporter, dest_addr=original_source). Plaintext — not
|
||||
/// end-to-end encrypted, since the transit router has no session with
|
||||
/// the source.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CoordsRequired {
|
||||
/// Destination that couldn't be routed.
|
||||
@@ -360,11 +322,12 @@ impl CoordsRequired {
|
||||
|
||||
/// Error indicating routing failure (local minimum or unreachable).
|
||||
///
|
||||
/// Sent back to the source when greedy routing fails.
|
||||
/// Carried inside a SessionDatagram addressed back to the original source.
|
||||
/// The reporting router creates a new SessionDatagram with src_addr=reporter
|
||||
/// and dest_addr=original_source, so the `original_src` field from the old
|
||||
/// design is no longer needed — it's the SessionDatagram's dest_addr.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathBroken {
|
||||
/// Original source of the failed packet.
|
||||
pub original_src: NodeAddr,
|
||||
/// Destination that couldn't be reached.
|
||||
pub dest_addr: NodeAddr,
|
||||
/// Node that detected the failure.
|
||||
@@ -375,9 +338,8 @@ pub struct PathBroken {
|
||||
|
||||
impl PathBroken {
|
||||
/// Create a new PathBroken error.
|
||||
pub fn new(original_src: NodeAddr, dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
|
||||
pub fn new(dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
|
||||
Self {
|
||||
original_src,
|
||||
dest_addr,
|
||||
reporter,
|
||||
last_known_coords: None,
|
||||
@@ -457,39 +419,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_data_packet_size() {
|
||||
let packet = DataPacket::new(make_node_addr(1), make_node_addr(2), vec![0u8; 100]);
|
||||
let packet = DataPacket::new(vec![0u8; 100]);
|
||||
|
||||
// 36 byte header + 100 byte payload
|
||||
assert_eq!(packet.total_size(), 136);
|
||||
assert_eq!(packet.header_size(), 36);
|
||||
// 4 byte header + 100 byte payload
|
||||
assert_eq!(packet.total_size(), 104);
|
||||
assert_eq!(packet.header_size(), 4);
|
||||
assert_eq!(packet.payload_len(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_packet_hop_limit() {
|
||||
let mut packet = DataPacket::new(make_node_addr(1), make_node_addr(2), vec![]);
|
||||
|
||||
packet.hop_limit = 2;
|
||||
assert!(packet.can_forward());
|
||||
|
||||
assert!(packet.decrement_hop_limit());
|
||||
assert_eq!(packet.hop_limit, 1);
|
||||
|
||||
assert!(packet.decrement_hop_limit());
|
||||
assert_eq!(packet.hop_limit, 0);
|
||||
assert!(!packet.can_forward());
|
||||
|
||||
assert!(!packet.decrement_hop_limit());
|
||||
assert_eq!(packet.hop_limit, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_packet_builder() {
|
||||
let packet = DataPacket::new(make_node_addr(1), make_node_addr(2), vec![1, 2, 3])
|
||||
.with_hop_limit(32)
|
||||
let packet = DataPacket::new(vec![1, 2, 3])
|
||||
.with_flags(DataFlags::from_byte(0x80));
|
||||
|
||||
assert_eq!(packet.hop_limit, 32);
|
||||
assert_eq!(packet.flags.to_byte(), 0x80);
|
||||
}
|
||||
|
||||
@@ -525,13 +467,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_session_setup() {
|
||||
let setup = SessionSetup::new(
|
||||
make_node_addr(1),
|
||||
make_node_addr(2),
|
||||
make_coords(&[1, 0]),
|
||||
make_coords(&[2, 0]),
|
||||
)
|
||||
.with_flags(SessionFlags::new().with_ack());
|
||||
let setup = SessionSetup::new(make_coords(&[1, 0]), make_coords(&[2, 0]))
|
||||
.with_flags(SessionFlags::new().with_ack());
|
||||
|
||||
assert!(setup.flags.request_ack);
|
||||
assert!(!setup.flags.bidirectional);
|
||||
@@ -551,9 +488,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_path_broken() {
|
||||
let err = PathBroken::new(make_node_addr(1), make_node_addr(2), make_node_addr(3))
|
||||
let err = PathBroken::new(make_node_addr(2), make_node_addr(3))
|
||||
.with_last_coords(make_coords(&[2, 0]));
|
||||
|
||||
assert_eq!(err.dest_addr, make_node_addr(2));
|
||||
assert_eq!(err.reporter, make_node_addr(3));
|
||||
assert!(err.last_known_coords.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user