195 lines
6.6 KiB
Zig
195 lines
6.6 KiB
Zig
//! Relay Service - Layer 2 Packet Forwarding
|
|
//!
|
|
//! This service handles incoming relay packets, unwraps them,
|
|
//! and forwards them to the next hop in the circuit.
|
|
|
|
const std = @import("std");
|
|
const l0_transport = @import("l0_transport");
|
|
const relay_mod = l0_transport.relay;
|
|
const dht_mod = l0_transport.dht;
|
|
|
|
pub const RelayService = struct {
|
|
pub const SessionContext = struct {
|
|
packet_count: u64,
|
|
last_seen: i64,
|
|
};
|
|
|
|
allocator: std.mem.Allocator,
|
|
onion_builder: relay_mod.OnionBuilder,
|
|
|
|
// Statistics
|
|
packets_forwarded: u64,
|
|
packets_dropped: u64,
|
|
sessions: std.AutoHashMap([16]u8, SessionContext),
|
|
|
|
pub fn init(allocator: std.mem.Allocator) RelayService {
|
|
return .{
|
|
.allocator = allocator,
|
|
.onion_builder = relay_mod.OnionBuilder.init(allocator),
|
|
.packets_forwarded = 0,
|
|
.packets_dropped = 0,
|
|
.sessions = std.AutoHashMap([16]u8, SessionContext).init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *RelayService) void {
|
|
self.sessions.deinit();
|
|
}
|
|
|
|
/// Forward a relay packet to the next hop
|
|
/// Returns the next hop address and the inner payload
|
|
/// Forward a relay packet to the next hop
|
|
/// Returns the next hop address and the inner payload
|
|
pub fn forwardPacket(
|
|
self: *RelayService,
|
|
raw_packet: []const u8,
|
|
receiver_private_key: [32]u8,
|
|
) !relay_mod.RelayResult {
|
|
// Parse the wire packet
|
|
var packet = try relay_mod.RelayPacket.decode(self.allocator, raw_packet);
|
|
defer packet.deinit(self.allocator);
|
|
|
|
// Unwrap the onion layer (using our private key + packet's ephemeral key)
|
|
const result = try self.onion_builder.unwrapLayer(packet, receiver_private_key, null);
|
|
|
|
// Check if next_hop is all zeros (meaning we're the final destination)
|
|
const is_final = blk: {
|
|
for (result.next_hop) |b| {
|
|
if (b != 0) break :blk false;
|
|
}
|
|
break :blk true;
|
|
};
|
|
|
|
if (is_final) {
|
|
// We're the final destination - deliver locally
|
|
std.log.info("Relay: Final destination reached for session {x}", .{result.session_id});
|
|
self.packets_dropped += 1; // Not actually dropped, just not forwarded
|
|
return result;
|
|
}
|
|
|
|
// Forward to next hop
|
|
std.log.debug("Relay: Forwarding session {x} to next hop: {x}", .{ result.session_id, result.next_hop });
|
|
|
|
// Update Sticky Session Stats
|
|
const now = std.time.timestamp();
|
|
const gop = try self.sessions.getOrPut(result.session_id);
|
|
if (!gop.found_existing) {
|
|
gop.value_ptr.* = .{ .packet_count = 1, .last_seen = now };
|
|
std.log.info("Relay: New Sticky Session detected: {x}", .{result.session_id});
|
|
} else {
|
|
gop.value_ptr.packet_count += 1;
|
|
gop.value_ptr.last_seen = now;
|
|
}
|
|
|
|
self.packets_forwarded += 1;
|
|
|
|
// Result payload includes the re-wrapped inner onion?
|
|
// Wait, unwrapLayer returns the decrypted payload.
|
|
// In onion routing, the decrypted payload IS the inner onion for the next hop.
|
|
// We just return it. The caller (node.zig) must send it.
|
|
return result;
|
|
}
|
|
|
|
/// Prune inactive sessions (Garbage Collection)
|
|
/// Removes sessions inactive for more than max_age_seconds
|
|
/// Returns number of sessions removed
|
|
pub fn pruneSessions(self: *RelayService, max_age_seconds: u64) !usize {
|
|
const now = std.time.timestamp();
|
|
var expired_keys = std.ArrayList([16]u8).init(self.allocator);
|
|
defer expired_keys.deinit();
|
|
|
|
var it = self.sessions.iterator();
|
|
while (it.next()) |entry| {
|
|
const age = now - entry.value_ptr.last_seen;
|
|
if (age > @as(i64, @intCast(max_age_seconds))) {
|
|
try expired_keys.append(entry.key_ptr.*);
|
|
}
|
|
}
|
|
|
|
for (expired_keys.items) |key| {
|
|
_ = self.sessions.remove(key);
|
|
}
|
|
|
|
return expired_keys.items.len;
|
|
}
|
|
|
|
/// Get relay statistics
|
|
pub fn getStats(self: *const RelayService) RelayStats {
|
|
return .{
|
|
.packets_forwarded = self.packets_forwarded,
|
|
.packets_dropped = self.packets_dropped,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const RelayStats = struct {
|
|
packets_forwarded: u64,
|
|
packets_dropped: u64,
|
|
};
|
|
|
|
test "RelayService: Forward packet" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var relay_service = RelayService.init(allocator);
|
|
defer relay_service.deinit();
|
|
|
|
// Create a test packet
|
|
const payload = "Test payload";
|
|
const next_hop = [_]u8{0xAB} ** 32;
|
|
// const shared_secret = [_]u8{0} ** 32; // Not used directly anymore, using private key
|
|
|
|
// Generate keys
|
|
const receiver_kp = std.crypto.dh.X25519.KeyPair.generate();
|
|
const receiver_pub = receiver_kp.public_key;
|
|
const receiver_priv = receiver_kp.secret_key;
|
|
|
|
const session_id = [_]u8{0x11} ** 16;
|
|
|
|
var onion_builder = relay_mod.OnionBuilder.init(allocator);
|
|
// Wrap layer targeting the receiver
|
|
var packet = try onion_builder.wrapLayer(payload, next_hop, receiver_pub, session_id, null);
|
|
defer packet.deinit(allocator);
|
|
|
|
const encoded = try packet.encode(allocator);
|
|
defer allocator.free(encoded);
|
|
|
|
// Forward the packet (pass encoded bytes)
|
|
const result = try relay_service.forwardPacket(encoded, receiver_priv);
|
|
defer allocator.free(result.payload);
|
|
|
|
try std.testing.expectEqualSlices(u8, &next_hop, &result.next_hop);
|
|
try std.testing.expectEqualSlices(u8, payload, result.payload);
|
|
try std.testing.expectEqualSlices(u8, &session_id, &result.session_id);
|
|
|
|
// Check stats
|
|
const stats = relay_service.getStats();
|
|
try std.testing.expectEqual(@as(u64, 1), stats.packets_forwarded);
|
|
}
|
|
|
|
test "RelayService: Session cleanup" {
|
|
const allocator = std.testing.allocator;
|
|
var service = RelayService.init(allocator);
|
|
defer service.deinit();
|
|
|
|
const session_id = [_]u8{0xAA} ** 16;
|
|
const now = std.time.timestamp();
|
|
|
|
// Add old session (2 hours ago)
|
|
try service.sessions.put(session_id, .{
|
|
.packet_count = 10,
|
|
.last_seen = now - 7200,
|
|
});
|
|
|
|
// Add fresh session (10 seconds ago)
|
|
const fresh_id = [_]u8{0xBB} ** 16;
|
|
try service.sessions.put(fresh_id, .{
|
|
.packet_count = 5,
|
|
.last_seen = now - 10,
|
|
});
|
|
|
|
const removed = try service.pruneSessions(3600); // 1 hour max age
|
|
try std.testing.expectEqual(@as(usize, 1), removed);
|
|
try std.testing.expect(service.sessions.get(session_id) == null);
|
|
try std.testing.expect(service.sessions.get(fresh_id) != null);
|
|
}
|