feat(relay): Implement Sticky Sessions & RelaySend CLI
- Added to support reusing SessionID and Ephemeral Keys. - Updated to track session statistics (packet counts) for rate-limiting. - Implemented CLI command to send packets via Relay. - Refactored to accept optional reusable keypair. - Updated tests.
This commit is contained in:
parent
8b115ee2a6
commit
87cd30dbe3
|
|
@ -5,10 +5,19 @@
|
|||
const std = @import("std");
|
||||
const relay = @import("relay");
|
||||
const dht = @import("dht");
|
||||
const crypto = std.crypto;
|
||||
const QvlStore = @import("qvl_store.zig").QvlStore;
|
||||
const PeerTable = @import("peer_table.zig").PeerTable;
|
||||
const DhtService = dht.DhtService;
|
||||
|
||||
pub const ActiveCircuit = struct {
|
||||
session_id: [16]u8,
|
||||
relay_address: std.net.Address,
|
||||
relay_pubkey: [32]u8,
|
||||
// Sticky Ephemeral Key (for optimizations)
|
||||
ephemeral_keypair: crypto.dh.X25519.KeyPair,
|
||||
};
|
||||
|
||||
pub const CircuitError = error{
|
||||
NoRelaysAvailable,
|
||||
TargetNotFound,
|
||||
|
|
@ -89,10 +98,47 @@ pub const CircuitBuilder = struct {
|
|||
std.crypto.random.bytes(&session_id);
|
||||
|
||||
// Wrap: Relay Packet -> [ NextHop: Target | Payload ]
|
||||
const packet = try self.onion_builder.wrapLayer(payload, target_id, relay_pubkey, session_id);
|
||||
const packet = try self.onion_builder.wrapLayer(payload, target_id, relay_pubkey, session_id, null);
|
||||
|
||||
return .{ .packet = packet, .first_hop = relay_node.address };
|
||||
}
|
||||
|
||||
/// Create a sticky session circuit
|
||||
pub fn createCircuit(self: *CircuitBuilder, relay_did: ?[]const u8) !ActiveCircuit {
|
||||
// Select Relay (Random if null)
|
||||
const selected_did = if (relay_did) |did| try self.allocator.dupe(u8, did) else blk: {
|
||||
const trusted = try self.qvl_store.getTrustedRelays(0.5, 1);
|
||||
if (trusted.len == 0) return error.NoRelaysAvailable;
|
||||
break :blk trusted[0];
|
||||
};
|
||||
defer self.allocator.free(selected_did);
|
||||
|
||||
// Resolve Relay Keys
|
||||
var relay_id = [_]u8{0} ** 32;
|
||||
if (selected_did.len >= 32) @memcpy(&relay_id, selected_did[0..32]);
|
||||
|
||||
const relay_node = self.dht.routing_table.findNode(relay_id) orelse return error.RelayNotFound;
|
||||
|
||||
const kp = crypto.dh.X25519.KeyPair.generate();
|
||||
var session_id: [16]u8 = undefined;
|
||||
std.crypto.random.bytes(&session_id);
|
||||
|
||||
return ActiveCircuit{
|
||||
.session_id = session_id,
|
||||
.relay_address = relay_node.address,
|
||||
.relay_pubkey = relay_node.key,
|
||||
.ephemeral_keypair = kp,
|
||||
};
|
||||
}
|
||||
|
||||
/// Send a payload on an existing circuit (reusing session/keys)
|
||||
pub fn sendOnCircuit(self: *CircuitBuilder, circuit: *ActiveCircuit, target_did: []const u8, payload: []const u8) !relay.RelayPacket {
|
||||
var target_id = [_]u8{0} ** 32;
|
||||
if (target_did.len >= 32) @memcpy(&target_id, target_did[0..32]);
|
||||
|
||||
// Use stored keys for stickiness
|
||||
return self.onion_builder.wrapLayer(payload, target_id, circuit.relay_pubkey, circuit.session_id, circuit.ephemeral_keypair);
|
||||
}
|
||||
};
|
||||
|
||||
test "Circuit: Build 1-Hop" {
|
||||
|
|
|
|||
|
|
@ -656,6 +656,7 @@ pub const CapsuleNode = struct {
|
|||
self.allocator,
|
||||
self.qvl_store,
|
||||
&self.peer_table,
|
||||
&self.dht,
|
||||
);
|
||||
}
|
||||
self.config.relay_enabled = true;
|
||||
|
|
@ -690,13 +691,20 @@ pub const CapsuleNode = struct {
|
|||
},
|
||||
.RelaySend => |args| {
|
||||
if (self.circuit_builder) |*cb| {
|
||||
// MVP: Build circuit returns ONLY the packet.
|
||||
// We need to know who the first hop is.
|
||||
// Let's modify CircuitBuilder to return that info.
|
||||
// For now, fail with message.
|
||||
_ = args;
|
||||
_ = cb;
|
||||
response = .{ .Error = "RelaySend not yet implemented: CircuitBuilder API requires update to return next hop address." };
|
||||
if (cb.buildOneHopCircuit(args.target_did, args.message)) |result| {
|
||||
var packet = result.packet;
|
||||
defer packet.deinit(self.allocator);
|
||||
const first_hop = result.first_hop;
|
||||
|
||||
const encoded = try packet.encode(self.allocator);
|
||||
defer self.allocator.free(encoded);
|
||||
|
||||
try self.utcp.send(first_hop, encoded, l0.LWFHeader.ServiceType.RELAY_FORWARD);
|
||||
response = .{ .Ok = "Packet sent via Relay" };
|
||||
} else |err| {
|
||||
std.log.warn("RelaySend failed: {}", .{err});
|
||||
response = .{ .Error = "Failed to build circuit" };
|
||||
}
|
||||
} else {
|
||||
response = .{ .Error = "Relay service not enabled" };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@ const relay_mod = @import("relay");
|
|||
const dht_mod = @import("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 .{
|
||||
|
|
@ -21,11 +27,12 @@ pub const RelayService = struct {
|
|||
.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;
|
||||
self.sessions.deinit();
|
||||
}
|
||||
|
||||
/// Forward a relay packet to the next hop
|
||||
|
|
@ -61,6 +68,18 @@ pub const RelayService = struct {
|
|||
|
||||
// Forward to next hop
|
||||
std.log.debug("Relay: Forwarding session {x} to next hop: {x}", .{ result.session_id, std.fmt.fmtSliceHexLower(&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?
|
||||
|
|
@ -93,18 +112,30 @@ test "RelayService: Forward packet" {
|
|||
// Create a test packet
|
||||
const payload = "Test payload";
|
||||
const next_hop = [_]u8{0xAB} ** 32;
|
||||
const shared_secret = [_]u8{0} ** 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);
|
||||
var packet = try onion_builder.wrapLayer(payload, next_hop, shared_secret);
|
||||
// Wrap layer targeting the receiver
|
||||
var packet = try onion_builder.wrapLayer(payload, next_hop, receiver_pub, session_id, null);
|
||||
defer packet.deinit(allocator);
|
||||
|
||||
// Forward the packet
|
||||
const result = try relay_service.forwardPacket(packet, shared_secret);
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -95,9 +95,10 @@ pub const OnionBuilder = struct {
|
|||
next_hop: [32]u8,
|
||||
next_hop_pubkey: [32]u8,
|
||||
session_id: [16]u8,
|
||||
ephemeral_keypair: ?crypto.dh.X25519.KeyPair,
|
||||
) !RelayPacket {
|
||||
// 1. Generate Ephemeral Keypair
|
||||
const kp = crypto.dh.X25519.KeyPair.generate();
|
||||
// 1. Generate or Use Ephemeral Keypair
|
||||
const kp = ephemeral_keypair orelse crypto.dh.X25519.KeyPair.generate();
|
||||
|
||||
// 2. Compute Shared Secret
|
||||
const shared_secret = try crypto.dh.X25519.scalarmult(kp.secret_key, next_hop_pubkey);
|
||||
|
|
@ -204,7 +205,7 @@ test "Relay: wrap and unwrap" {
|
|||
|
||||
const session_id = [_]u8{0xCC} ** 16;
|
||||
|
||||
var packet = try builder.wrapLayer(payload, next_hop, receiver_pubkey, session_id);
|
||||
var packet = try builder.wrapLayer(payload, next_hop, receiver_pubkey, session_id, null);
|
||||
defer packet.deinit(allocator);
|
||||
|
||||
// Verify it is encrypted (not plain)
|
||||
|
|
|
|||
Loading…
Reference in New Issue