feat: add snapshot based simulation test

This commit is contained in:
Arran Ireland
2025-07-04 13:14:04 +01:00
parent 7ff7b7dcfb
commit a4d68dc822
34 changed files with 1490 additions and 665 deletions

View File

@@ -2,15 +2,6 @@
An implementation of [Reticulum](https://github.com/markqvist/Reticulum) in [Zig](https://ziglang.org/) targeting operating systems and embedded devices.
# Roadmap
- Implement core transport.
- Test core transport.
- Implement a transport node pico build.
- Test transport node pico build.
- Implement app.
- Test app.
# Structure
## App
@@ -26,14 +17,13 @@ An implementation of [Reticulum](https://github.com/markqvist/Reticulum) in [Zig
- The crypto implementation currently leverages std.crypto from the zig std library.
- Eventually I will provide an option to use OpenSSL.
## Hardware
## Boards
- `hardware/` stores image setups for embedded devices that users can build with one command.
- `boards/` stores image setups for embedded devices that users can build with one command.
- Makes use of [microzig](https://github.com/ZigEmbeddedGroup/microzig) as a submodule for targeting embedded devices.
- Currently there is a very simple proof of concept for the pico.
- It makes an identity, endpoint and announce packet and sends it over serial.
- The image can be built by running `zig build -Doptimize=ReleaseSafe pico` from `hardware`.
- Note that the upstream USB CBC code is known to be buggy.
- The image can be built by running `zig build -Doptimize=ReleaseSafe pico` from `boards`.
## Test
@@ -53,3 +43,7 @@ An implementation of [Reticulum](https://github.com/markqvist/Reticulum) in [Zig
# Anti-goals
- Exact parity to reference implementation in terminology/structure.
# Licence
- Currently under Apache 2.0 for now; if I move over to the Reticulum licence at some point, it will only be after significant thought and consideration.

View File

@@ -4,6 +4,7 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Core module.
const core = .{
.name = "reticulum",
.module = b.addModule("reticulum-core", .{
@@ -13,6 +14,7 @@ pub fn build(b: *std.Build) void {
}),
};
// All tests.
const test_step = b.step("test", "Run all tests.");
// Unit tests.
@@ -37,33 +39,41 @@ pub fn build(b: *std.Build) void {
unit_tests_step.dependOn(&b.addRunArtifact(t_core).step);
}
// Integration tests.
// Deterministic simulation tests.
{
const integration_tests_step = b.step("integration-tests", "Run integration tests.");
test_step.dependOn(integration_tests_step);
const simulation_tests_step = b.step("simulation-tests", "Run simulation tests.");
test_step.dependOn(simulation_tests_step);
const fixtures = b.createModule(.{
.root_source_file = b.path("test/fixtures.zig"),
.target = target,
.optimize = optimize,
.imports = &.{core},
});
const integration_tests = .{
"announce",
const ohsnap = blk: {
// Root directories map to module names.
const module_names: []const []const u8 = &.{
"root",
};
const root_directory: []const []const u8 = &.{
"test/simulation",
};
break :blk b.dependency("ohsnap", .{
.target = target,
.optimize = optimize,
.module_name = module_names,
.root_directory = root_directory,
});
};
// const ohsnap = b.dependency("ohsnap", .{});s
inline for (integration_tests) |name| {
const simulation_tests = .{
"plain",
};
inline for (simulation_tests) |name| {
const t = b.addTest(.{
.name = name,
.root_source_file = b.path("test/integration/" ++ name ++ ".zig"),
.root_source_file = b.path("test/simulation/" ++ name ++ ".zig"),
.target = target,
.optimize = optimize,
});
t.root_module.addImport("fixtures", fixtures);
t.root_module.addImport(core.name, core.module);
integration_tests_step.dependOn(&b.addRunArtifact(t).step);
t.root_module.addImport("ohsnap", ohsnap.module("ohsnap"));
simulation_tests_step.dependOn(&b.addRunArtifact(t).step);
}
}
}

View File

@@ -1,19 +1,20 @@
.{
.name = "reticulum",
.version = "0.1.0",
.minimum_zig_version = "0.13.0",
.name = .reticulum,
.version = "0.2.0",
.minimum_zig_version = "0.14.1",
.fingerprint = 0x357e04a91bafe743,
.paths = .{
"build.zig",
"build.zig.zon",
"LICENSE",
"README.md",
"src/",
"core/",
},
.dependencies = .{
// TODO: Use in integration testing.
// .ohsnap = .{
// .url = "https://github.com/mnemnion/ohsnap/archive/refs/tags/v0.3.1.tar.gz",
// .hash = "1220380908ede3cce3dafb2e69b4994a3df55a468597d9d571a495c66ad38ac73ef8",
// },
// TODO: Move this out into just the tests if possible. Currently zig doesn't have dev dependencies.
.ohsnap = .{
.url = "git+https://github.com/mnemnion/ohsnap#a140626e388ad3aea2a6a678183f5c0c03887fde",
.hash = "ohsnap-0.3.1-iWxzyu6bAADX1OdmK7FFg94X6RDfDvbt0iwQFH8mSFJE",
},
},
}

View File

@@ -1,44 +1,53 @@
const std = @import("std");
const BitRate = @import("units.zig").BitRate;
const data = @import("data.zig");
pub const Manager = @import("interface/Manager.zig");
const Allocator = std.mem.Allocator;
const Element = @import("Node.zig").Element;
const BitRate = @import("unit.zig").BitRate;
const Event = @import("Node.zig").Event;
const Endpoint = @import("endpoint.zig").Managed;
const Hash = @import("crypto.zig").Hash;
const Packet = @import("packet.zig").Packet;
const PacketBuilder = @import("packet.zig").Builder;
const PacketFactory = @import("packet.zig").Factory;
const Payload = @import("packet.zig").Payload;
const Name = @import("endpoint/Name.zig");
const ThreadSafeFifo = @import("internal/ThreadSafeFifo.zig").ThreadSafeFifo;
// Probably rework this file.
pub const Id = usize;
pub const Incoming = ThreadSafeFifo(Element.In);
pub const Outgoing = ThreadSafeFifo(Element.Out);
pub const Mode = enum {
full,
point_to_point,
access_point,
roaming,
boundary,
gateway,
};
pub const Incoming = ThreadSafeFifo(Event.In);
pub const Outgoing = ThreadSafeFifo(Event.Out);
pub const Config = struct {
name: []const u8 = "unknown",
access_code: ?[]const u8 = null,
initial_bit_rate: BitRate = .{
.bits = .{
.count = 1,
.prefix = .kilo,
},
.rate = .per_second,
},
mode: Mode = .full,
initial_bit_rate: BitRate = BitRate.default,
max_held_packets: usize = 1000,
};
pub const Error = Incoming.Error || Outgoing.Error || PacketFactory.Error || Allocator.Error;
const ForCollection = std.fifo.LinearFifo(Packet, .Dynamic);
const Self = @This();
// TODO: Account for interfaces that only receive packets and don't transmit.
// TODO: Find a less error prone way to define the API.
// TODO: Rethink and refactor the event API.
ally: Allocator,
id: Id,
incoming: *Incoming,
outgoing: *Outgoing,
for_collection: ForCollection,
packet_factory: PacketFactory,
bit_rate: BitRate,
bit_rate: ?BitRate,
pub fn init(
ally: Allocator,
@@ -47,76 +56,105 @@ pub fn init(
incoming: *Incoming,
outgoing: *Outgoing,
packet_factory: PacketFactory,
) !Self {
) Self {
return Self{
.ally = ally,
.id = id,
.incoming = incoming,
.outgoing = outgoing,
.for_collection = ForCollection.init(ally),
.packet_factory = packet_factory,
.bit_rate = config.initial_bit_rate,
};
}
pub fn deliver_raw(ptr: *anyopaque, bytes: []const u8) !void {
const self: *Self = @ptrCast(@alignCast(ptr));
const packet = try self.packet_factory.from_bytes(bytes);
try deliver(ptr, packet);
pub fn announce(ptr: *anyopaque, hash: Hash, app_data: ?data.Bytes) Error!void {
try deliverEvent(ptr, Event.In{
.announce = .{
.hash = hash,
.app_data = app_data,
},
});
}
pub fn deliver(ptr: *anyopaque, packet: Packet) !void {
const self: *Self = @ptrCast(@alignCast(ptr));
try self.incoming.push(.{ .packet = packet });
pub fn plain(ptr: *anyopaque, name: Name, payload: Payload) Error!void {
try deliverEvent(ptr, Event.In{
.plain = .{
.name = name,
.payload = payload,
},
});
}
pub fn send(ptr: *anyopaque, packet: Packet) !void {
pub fn deliverRawPacket(ptr: *anyopaque, bytes: []const u8) !void {
const self: *Self = @ptrCast(@alignCast(ptr));
try self.outgoing.push(.{ .packet = packet });
const packet = try self.packet_factory.fromBytes(bytes);
try deliverPacket(ptr, packet);
}
pub fn collect(ptr: *anyopaque, current_bit_rate: BitRate) ?Packet {
pub fn deliverPacket(ptr: *anyopaque, packet: Packet) !void {
try deliverEvent(ptr, .{ .packet = packet });
}
pub fn deliverEvent(ptr: *anyopaque, event: Event.In) !void {
const self: *Self = @ptrCast(@alignCast(ptr));
self.bit_rate = current_bit_rate;
return self.for_collection.readItem();
try self.incoming.push(event);
}
pub fn collectEvent(ptr: *anyopaque) ?Event.Out {
const self: *Self = @ptrCast(@alignCast(ptr));
return self.outgoing.pop();
}
pub fn deinit(self: *Self) void {
self.incoming.deinit();
self.outgoing.deinit();
self.ally.destroy(self.incoming);
self.ally.destroy(self.outgoing);
self.* = undefined;
}
pub fn api(self: *Self) Api {
return Api{
return .{
.ptr = self,
.deliverRawFn = deliver_raw,
.deliverFn = deliver,
.sendFn = send,
.collectFn = collect,
.announceFn = announce,
.plainFn = plain,
.deliverRawPacketFn = deliverRawPacket,
.deliverPacketFn = deliverPacket,
.deliverEventFn = deliverEvent,
.collectEventFn = collectEvent,
};
}
pub const Api = struct {
ptr: *anyopaque,
deliverRawFn: *const fn (ptr: *anyopaque, raw_bytes: []const u8) Error!void,
deliverFn: *const fn (ptr: *anyopaque, packet: Packet) Error!void,
sendFn: *const fn (ptr: *anyopaque, packet: Packet) Error!void,
collectFn: *const fn (ptr: *anyopaque, current_bit_rate: BitRate) ?Packet,
announceFn: *const fn (ptr: *anyopaque, hash: Hash, app_data: ?data.Bytes) Error!void,
plainFn: *const fn (ptr: *anyopaque, name: Name, payload: Payload) Error!void,
deliverRawPacketFn: *const fn (ptr: *anyopaque, raw_bytes: []const u8) Error!void,
deliverPacketFn: *const fn (ptr: *anyopaque, packet: Packet) Error!void,
deliverEventFn: *const fn (ptr: *anyopaque, event: Event.In) Error!void,
collectEventFn: *const fn (ptr: *anyopaque) ?Event.Out,
pub fn announce(self: *@This(), endpoint: *const Endpoint, application_data: ?[]const u8) Error!void {
const engine: *Self = @ptrCast(@alignCast(self.ptr));
const packet = try engine.packet_factory.make_announce(endpoint, application_data);
try self.send(packet);
pub fn announce(self: *@This(), hash: Hash, app_data: ?data.Bytes) Error!void {
return self.announceFn(self.ptr, hash, app_data);
}
pub fn deliver_raw(self: *@This(), raw_bytes: []const u8) Error!void {
return self.deliverRawFn(self.ptr, raw_bytes);
pub fn plain(self: *@This(), name: Name, payload: Payload) Error!void {
return self.plainFn(self.ptr, name, payload);
}
pub fn deliver(self: *@This(), packet: Packet) Error!void {
return self.deliverFn(self.ptr, packet);
pub fn deliverRawPacket(self: *@This(), raw_bytes: []const u8) Error!void {
return self.deliverRawPacketFn(self.ptr, raw_bytes);
}
pub fn send(self: *@This(), packet: Packet) Error!void {
return self.sendFn(self.ptr, packet);
pub fn deliverPacket(self: *@This(), packet: Packet) Error!void {
return self.deliverPacketFn(self.ptr, packet);
}
pub fn collect(self: *@This(), current_bit_rate: BitRate) ?Packet {
return self.collectFn(self.ptr, current_bit_rate);
pub fn deliverEvent(self: *@This(), event: Event.In) Error!void {
return self.deliverEventFn(self.ptr, event);
}
pub fn collectEvent(self: *@This()) ?Event.Out {
return self.collectEventFn(self.ptr);
}
};

View File

@@ -1,57 +1,294 @@
const std = @import("std");
pub const Element = @import("node/Element.zig");
pub const Event = @import("node/Event.zig");
pub const Options = @import("node/Options.zig");
const Allocator = std.mem.Allocator;
const BitRate = @import("units.zig").BitRate;
const BitRate = @import("unit.zig").BitRate;
const Endpoint = @import("endpoint.zig").Managed;
const EndpointStore = @import("endpoint/Store.zig");
const EndpointBuilder = @import("endpoint.zig").Builder;
const Endpoints = @import("endpoint/Store.zig");
const Hash = @import("crypto.zig").Hash;
const Interface = @import("Interface.zig");
const Interfaces = @import("interface/Manager.zig");
const Identity = @import("crypto.zig").Identity;
const Packet = @import("packet.zig").Packet;
const PacketFactory = @import("packet.zig").Factory;
const Routes = @import("Routes.zig");
const Name = @import("endpoint/Name.zig");
const ThreadSafeFifo = @import("internal/ThreadSafeFifo.zig").ThreadSafeFifo;
const System = @import("System.zig");
pub const Error = error{
InterfaceNotFound,
TooManyInterfaces,
TooManyIncoming,
} || Allocator.Error;
const Route = struct {
timestamp: u64,
interface_id: Interface.Id,
next_hop: Hash.Short,
hops: u8,
// More fields.
};
pub const Error = Interfaces.Error || Identity.Error || EndpointBuilder.Error || Name.Error || Allocator.Error;
const Self = @This();
ally: Allocator,
mutex: std.Thread.Mutex,
system: System,
options: Options,
mutex: std.Thread.Mutex,
endpoints: EndpointStore,
interfaces: std.AutoHashMap(Interface.Id, *Interface),
routes: std.StringHashMap(Route),
current_interface_id: Interface.Id,
endpoints: Endpoints,
interfaces: Interfaces,
routes: Routes,
pub fn init(ally: Allocator, system: *System, identity: ?Identity, options: Options) Error!Self {
var endpoint_builder = EndpointBuilder.init(ally);
const main_endpoint = try endpoint_builder
.setIdentity(identity orelse try Identity.random(&system.rng))
.setDirection(.in)
.setMethod(.single)
.setName(try Name.init(options.name, &.{}, ally))
.build();
const endpoints = try Endpoints.init(ally, main_endpoint);
const interfaces = Interfaces.init(ally, system.*);
const routes = Routes.init(ally);
pub fn init(ally: Allocator, system: System, options: Options) Allocator.Error!Self {
return .{
.ally = ally,
.system = system,
.options = options,
.mutex = .{},
.endpoints = EndpointStore.init(ally),
.interfaces = std.AutoHashMap(Interface.Id, *Interface).init(ally),
.routes = std.StringHashMap(Route).init(ally),
.current_interface_id = 0,
.system = system.*,
.options = options,
.endpoints = endpoints,
.interfaces = interfaces,
.routes = routes,
};
}
pub fn addInterface(self: *Self, config: Interface.Config) Error!Interface.Api {
self.mutex.lock();
defer {
self.mutex.unlock();
}
return try self.interfaces.add(config);
}
pub fn removeInterface(self: *Self, id: Interface.Id) void {
self.mutex.lock();
defer {
self.mutex.unlock();
}
self.interfaces.remove(id);
}
pub fn process(self: *Self) !void {
self.mutex.lock();
defer {
self.mutex.unlock();
}
const now = self.system.clock.monotonicMicros();
var interfaces = self.interfaces.iterator();
while (interfaces.next()) |entry| {
try self.processEventsIn(now, entry.interface);
}
interfaces = self.interfaces.iterator();
while (interfaces.next()) |entry| {
try self.processEventsOut(now, entry.interface, &entry.pending_out);
}
}
fn processEventsIn(self: *Self, now: u64, interface: *Interface) !void {
while (interface.incoming.pop()) |event_in| {
var event = event_in;
defer {
event.deinit();
}
switch (event) {
.announce => |announce| {
if (self.endpoints.getPtr(announce.hash)) |endpoint| {
const app_data: ?[]const u8 = blk: {
if (announce.app_data) |app_data| {
break :blk app_data.items;
} else {
break :blk null;
}
};
const packet = try interface.packet_factory.makeAnnounce(endpoint, app_data);
try self.interfaces.propagate(packet, null);
}
},
.packet => |*packet| {
const header = packet.header;
defer {
packet.deinit();
}
if (shouldDrop(packet)) {
return;
}
try packet.validate();
if (shouldRemember(packet)) {
// Add packet hash to set.
}
packet.header.hops += 1;
try self.processTransport(now, packet);
try switch (header.purpose) {
.announce => self.processAnnounce(now, interface, packet),
.data => self.processData(now, packet),
.link_request => self.processLinkRequest(now, packet),
.proof => self.processProof(now, packet),
};
},
.plain => |plain| {
const packet = try interface.packet_factory.makePlain(plain.name, plain.payload);
try self.interfaces.propagate(packet, null);
},
}
}
}
fn processEventsOut(self: *Self, now: u64, interface: *Interface, pending_out: *Interface.Outgoing) !void {
while (pending_out.pop()) |event_out| {
var event = event_out;
var event_sent = false;
defer {
if (!event_sent) {
event.deinit();
}
}
switch (event) {
.packet => |*packet| {
const endpoint = packet.endpoints.endpoint();
_ = endpoint;
const purpose = packet.header.purpose;
const method = packet.header.method;
const hops = packet.header.hops;
if (purpose == .announce) {
try interface.outgoing.push(event);
event_sent = true;
continue;
}
if (method == .plain and hops == 0) {
try interface.outgoing.push(event);
event_sent = true;
continue;
}
// Put into transport if we know where it's going.
// Otherwise broadcast.
// Store the packet hash.
},
}
}
_ = self;
_ = now;
}
fn processTransport(self: *Self, now: u64, packet: *Packet) !void {
_ = now;
if (!self.options.transport_enabled) {
return;
}
if (packet.endpoints == .transport and packet.header.purpose != .announce) {
const next_hop = packet.endpoints.nextHop();
const our_identity = self.endpoints.main.identity orelse return Error.MissingIdentity;
const our_hash = our_identity.hash.short();
if (std.mem.eql(u8, &next_hop, our_hash)) {
const endpoint = packet.endpoints.endpoint();
if (try self.routes.hops(endpoint)) |hops| {
if (hops == 1) {
packet.endpoints = .{
.normal = .{
.endpoint = endpoint,
},
};
packet.header.format = .normal;
packet.header.propagation = .broadcast;
} else if (hops > 1) {
packet.endpoints.transport.transport_id = next_hop;
}
if (packet.header.purpose == .link_request) {
// Link request stuff.
} else {
// Add to reverse table [if_in, if_out, timestamp].
}
// Transmit.
// Update endpoint timestamp in table.
}
}
}
// Link transport.
if (packet.header.purpose != .announce and packet.header.purpose != .link_request and packet.context != .link_request_proof) {
// If packet endpoint not in link table, return.
// Otherwise do link table stuff and transmit.
}
}
fn processAnnounce(self: *Self, now: u64, interface: *Interface, packet: *Packet) !void {
try self.routes.update_from(packet, interface, now);
if (!self.options.transport_enabled) {
return;
}
// Propagating for now - will add the more sophisticated announce logic later.
var announce = try packet.clone();
defer {
announce.deinit();
}
try announce.setTransport(self.endpoints.main.hash.short());
try self.interfaces.propagate(announce, interface.id);
}
fn processData(self: *Self, now: u64, packet: *Packet) !void {
_ = self;
_ = now;
_ = packet;
}
fn processLinkRequest(self: *Self, now: u64, packet: *Packet) !void {
_ = self;
_ = now;
_ = packet;
}
fn processProof(self: *Self, now: u64, packet: *Packet) !void {
_ = self;
_ = now;
_ = packet;
}
fn shouldDrop(packet: *const Packet) bool {
_ = packet;
return false;
}
fn shouldRemember(packet: *const Packet) bool {
_ = packet;
return true;
}
pub fn deinit(self: *Self) void {
self.mutex.lock();
@@ -62,138 +299,5 @@ pub fn deinit(self: *Self) void {
self.endpoints.deinit();
self.interfaces.deinit();
self.incoming.deinit(self.ally);
self.outgoing.deinit(self.ally);
self.routes.deinit();
}
pub fn addInterface(self: *Self, config: Interface.Config) Error!Interface.Api {
self.mutex.lock();
defer {
self.mutex.unlock();
}
if (self.interfaces.count() > self.options.max_interfaces) {
return Error.TooManyInterfaces;
}
const id = self.current_interface_id;
self.current_interface_id += 1;
const incoming = try self.ally.create(Interface.Incoming);
const outgoing = try self.ally.create(Interface.Outgoing);
incoming.* = try Interface.Incoming.init(self.ally);
outgoing.* = try Interface.Outgoing.init(self.ally);
const packet_factory = PacketFactory.init(self.ally, self.system.clock, self.system.rng, config);
const engine = try self.ally.create(Interface);
engine.* = try Interface.init(self.ally, config, id, incoming, outgoing, packet_factory);
try self.interfaces.put(id, engine);
return engine.api();
}
pub fn removeInterface(self: *Self, id: Interface.Id) Error!void {
self.mutex.lock();
defer {
self.mutex.unlock();
}
if (self.interfaces.get(id)) |engine| {
engine.deinit(self.ally);
self.ally.destroy(engine);
return;
}
return Error.InterfaceNotFound;
}
pub fn process(self: *Self) !void {
self.mutex.lock();
defer {
self.mutex.unlock();
}
const now = self.system.clock.monotonicMicros();
try self.process_incoming(now);
try self.process_outgoing(now);
}
fn process_incoming(self: *Self, now: u64) !void {
var iterator = self.interfaces.iterator();
while (iterator.next()) |entry| {
var incoming = entry.value_ptr.*.incoming;
while (incoming.pop()) |element| {
var packet = element.packet;
var header = packet.header;
// defer {
// self.ally.free(element.packet.deinit());
// }
if (self.shouldDrop(&packet)) {
return;
}
header.hops += 1;
const is_valid = try packet.validate();
if (!is_valid) {
return;
}
if (header.purpose == .announce) {
const endpoint_hash = packet.endpoints.endpoint();
const next_hop = packet.endpoints.next_hop();
try self.routes.put(&endpoint_hash, Route{
.timestamp = now,
.interface_id = entry.key_ptr.*,
.next_hop = next_hop,
.hops = header.hops,
});
}
}
}
}
fn process_outgoing(self: *Self, now: u64) !void {
var iterator = self.interfaces.iterator();
_ = now;
while (iterator.next()) |entry| {
var outgoing = entry.value_ptr.*.outgoing;
while (outgoing.pop()) |element| {
var packet = element.packet;
const endpoint = packet.endpoints.endpoint();
if (packet.header.purpose == .announce) {
var interfaces = self.interfaces.valueIterator();
while (interfaces.next()) |interface| {
if (entry.value_ptr != interface) {
interface.outgoing.push(.{ .packet = packet });
}
}
return;
}
const route = self.routes.get(&endpoint) orelse return;
if (route.hops == 1) {
if (self.interfaces.get(entry.key_ptr.*)) |engine| {
try engine.outgoing.push(.{ .packet = packet });
}
} else {
// Modify the packet for transport.
}
}
}
}
fn shouldDrop(self: *Self, packet: *const Packet) bool {
_ = self;
_ = packet;
return false;
}

51
core/Routes.zig Normal file
View File

@@ -0,0 +1,51 @@
const std = @import("std");
const crypto = @import("crypto.zig");
const Allocator = std.mem.Allocator;
const Hash = crypto.Hash;
const Interface = @import("Interface.zig");
const Packet = @import("packet.zig").Managed;
const Self = @This();
const Entry = struct {
timestamp: u64,
interface_id: Interface.Id,
next_hop: Hash.Short,
hops: u8,
};
ally: Allocator,
entries: std.StringHashMap(Entry),
pub fn init(ally: Allocator) Self {
return Self{
.ally = ally,
.entries = std.StringHashMap(Entry).init(ally),
};
}
pub fn hops(self: *Self, endpoint: Hash.Short) !?u8 {
if (self.entries.get(&endpoint)) |entry| {
return entry.hops;
}
return null;
}
pub fn update_from(self: *Self, packet: *const Packet, interface: *const Interface, now: u64) !void {
const endpoint = packet.endpoints.endpoint();
const next_hop = packet.endpoints.nextHop();
try self.entries.put(&endpoint, Entry{
.timestamp = now,
.interface_id = interface.id,
.next_hop = next_hop,
.hops = packet.header.hops,
});
}
pub fn deinit(self: *Self) void {
self.entries.deinit();
self.* = undefined;
}

View File

@@ -1,18 +1,19 @@
const std = @import("std");
const Aes128 = std.crypto.core.aes.Aes128;
const Pkcs7 = @import("pkcs7.zig").Pkcs7;
const Aes = std.crypto.core.aes.Aes256;
const Pkcs7 = @import("pkcs7.zig").Impl;
const Self = @This();
pub const block_length = Aes128.block.block_length;
pub const key_length = Aes.key_bits / 8;
pub const block_length = Aes.block.block_length;
pub fn encrypt(dst: []u8, src: []const u8, key: [block_length]u8, iv: [block_length]u8) []u8 {
const context = Aes128.initEnc(key);
pub fn encrypt(dst: []u8, src: []const u8, key: [key_length]u8, iv: [block_length]u8) []u8 {
const context = Aes.initEnc(key);
var previous: *const [block_length]u8 = &iv;
var i: usize = 0;
while (i + block_length <= src.len) : (i += block_length) {
var current = Aes128.block.fromBytes(src[i .. i + block_length][0..block_length]);
var current = Aes.block.fromBytes(src[i .. i + block_length][0..block_length]);
const xored_block = current.xorBytes(previous);
var out = dst[i .. i + block_length];
context.encrypt(out[0..block_length], &xored_block);
@@ -24,13 +25,13 @@ pub fn encrypt(dst: []u8, src: []const u8, key: [block_length]u8, iv: [block_len
@memcpy(dst[i..src.len], src[i..src.len]);
}
const ciphertext = Pkcs7(block_length).pad(dst[0..src.len], dst);
const xored_block = Aes128.block.fromBytes(last_block).xorBytes(previous);
const xored_block = Aes.block.fromBytes(last_block).xorBytes(previous);
context.encrypt(last_block, &xored_block);
return ciphertext;
}
pub fn decrypt(dst: []u8, src: []const u8, key: [block_length]u8, iv: [block_length]u8) ![]const u8 {
const context = Aes128.initDec(key);
pub fn decrypt(dst: []u8, src: []const u8, key: [key_length]u8, iv: [block_length]u8) ![]const u8 {
const context = Aes.initDec(key);
var previous: *const [block_length]u8 = &iv;
var i: usize = 0;
@@ -40,7 +41,7 @@ pub fn decrypt(dst: []u8, src: []const u8, key: [block_length]u8, iv: [block_len
const current = src[i .. i + block_length][0..block_length];
const out = dst[i .. i + block_length];
context.decrypt(out[0..block_length], current);
const plaintext = Aes128.block.fromBytes(out[0..block_length]).xorBytes(previous);
const plaintext = Aes.block.fromBytes(out[0..block_length]).xorBytes(previous);
@memcpy(dst[i .. i + block_length], &plaintext);
previous = current;
}
@@ -49,7 +50,7 @@ pub fn decrypt(dst: []u8, src: []const u8, key: [block_length]u8, iv: [block_len
}
const t = std.testing;
const test_key = [Self.block_length]u8{ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF };
const test_key = [Self.key_length]u8{ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0xF, 0xE, 0xD, 0xC, 0xB, 0xA, 0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF };
const test_iv = [Self.block_length]u8{ 0xF, 0xE, 0xD, 0xC, 0xB, 0xA, 0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0 };
test "Encrypt and decrypt" {
@@ -84,7 +85,7 @@ test "Encrypt and decrypt empty" {
test "Encrypt and decrypt of block length" {
const initial_plaintext = "this is 16 chars";
var ciphertext_buffer: [2 * Self.block_length]u8 = undefined;
t.expect(initial_plaintext.len == Self.block_length);
try t.expect(initial_plaintext.len == Self.block_length);
const ciphertext = Self.encrypt(ciphertext_buffer[0..], initial_plaintext[0..], test_key, test_iv);
try t.expect(ciphertext.len % Self.block_length == 0);
@@ -100,7 +101,7 @@ test "Encrypt and decrypt of block length" {
test "Encrypt and decrypt of two block lengths" {
const initial_plaintext = "this is a total of 32 chars wide";
var ciphertext_buffer: [3 * Self.block_length]u8 = undefined;
t.expect(initial_plaintext.len == 2 * Self.block_length);
try t.expect(initial_plaintext.len == 2 * Self.block_length);
const ciphertext = Self.encrypt(ciphertext_buffer[0..], initial_plaintext[0..], test_key, test_iv);
try t.expect(ciphertext.len % Self.block_length == 0);

View File

@@ -1,4 +1,6 @@
const std = @import("std");
const data = @import("../data.zig");
const Sha256 = std.crypto.hash.sha2.Sha256;
const Self = @This();
@@ -19,11 +21,12 @@ pub fn from_long(hash: Long) Self {
};
}
pub fn hash_data(data: []const u8) Self {
return hash_items(.{ .data = data });
pub fn ofData(bytes: []const u8) Self {
return ofItems(.{ .bytes = bytes });
}
pub fn hash_items(items: anytype) Self {
// TODO: Refactor out magic values and handle more cases.
pub fn ofItems(items: anytype) Self {
var hash = Self{ .bytes = undefined };
var hasher = Sha256.init(.{});
@@ -31,17 +34,17 @@ pub fn hash_items(items: anytype) Self {
const value = @field(items, field.name);
switch (@TypeOf(value)) {
[]const u8, []u8 => hasher.update(value),
[]const std.ArrayList(u8) => |lists| {
for (lists) |l| {
hasher.update(l.items);
[]const data.Bytes => |bytes_list| {
for (bytes_list) |bytes| {
hasher.update(bytes.items);
}
},
*const Long => hasher.update(value),
*const Short => hasher.update(value),
*const Name => hasher.update(value),
[long_length]u8 => hasher.update(&value),
[name_length]u8 => hasher.update(&value),
else => switch (@typeInfo(@TypeOf(value))) {
.Int => hasher.update(std.mem.asBytes(&value)),
.Array => |_| hasher.update(std.mem.sliceAsBytes(value[0..])),
else => @compileError("Unsupported type: " ++ @typeName(@TypeOf(value))),
},
}

View File

@@ -79,12 +79,12 @@ pub fn signer(self: *const Self, rng: *Rng) Error!Ed25519.Signer {
return Error.MissingSecretKey;
}
pub fn has_secret(self: *Self) bool {
pub fn hasSecret(self: *Self) bool {
return self.secret != null;
}
fn make_hash(public: Public) Hash {
return Hash.hash_items(.{
return Hash.ofItems(.{
.dh = public.dh,
.signature = public.signature.bytes,
});

View File

@@ -1,6 +1,6 @@
const std = @import("std");
pub fn Pkcs7(comptime block_size: u8) type {
pub fn Impl(comptime block_size: u8) type {
return struct {
pub const Error = error{
InvalidLength,
@@ -46,7 +46,7 @@ pub fn Pkcs7(comptime block_size: u8) type {
}
const t = std.testing;
const P = Pkcs7(16);
const P = Impl(16);
test "pad - empty" {
const data: [0]u8 = undefined;

10
core/data.zig Normal file
View File

@@ -0,0 +1,10 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const Bytes = std.ArrayList(u8);
pub fn makeBytes(slice: []const u8, ally: Allocator) !Bytes {
var bytes = Bytes.init(ally);
try bytes.appendSlice(slice);
return bytes;
}

View File

@@ -1,5 +1,6 @@
pub const Builder = @import("endpoint/Builder.zig");
pub const Managed = @import("endpoint/Managed.zig");
pub const Name = @import("endpoint/Name.zig");
pub const Store = @import("endpoint/Store.zig");
pub const Direction = enum {

View File

@@ -1,130 +1,102 @@
const std = @import("std");
const crypto = @import("../crypto.zig");
const data = @import("../data.zig");
const endpoint = @import("../endpoint.zig");
const Allocator = std.mem.Allocator;
const Bytes = std.ArrayList(u8);
const Identity = crypto.Identity;
const Direction = endpoint.Direction;
const Method = endpoint.Method;
const Name = endpoint.Name;
const Hash = crypto.Hash;
const Managed = @import("Managed.zig");
const Self = @This();
const Fields = std.bit_set.IntegerBitSet(std.meta.fields(Managed).len - 4);
pub const Error = error{
Incomplete,
InvalidName,
InvalidAspect,
HasIdentity,
MissingIdentity,
MissingDirection,
MissingMethod,
MissingName,
} || Allocator.Error;
ally: Allocator,
fields: Fields,
identity: Identity = undefined,
direction: Direction = undefined,
method: Method = undefined,
application_name: Bytes,
aspects: std.ArrayList(Bytes),
identity: ?Identity,
direction: ?Direction,
method: ?Method,
name: ?Name,
pub fn init(ally: Allocator) Self {
return Self{
.ally = ally,
.fields = Fields.initEmpty(),
.application_name = Bytes.init(ally),
.aspects = std.ArrayList(Bytes).init(ally),
.identity = null,
.direction = null,
.method = null,
.name = null,
};
}
pub fn set_identity(self: *Self, identity: Identity) *Self {
self.fields.set(0);
pub fn setIdentity(self: *Self, identity: Identity) *Self {
self.identity = identity;
return self;
}
pub fn set_direction(self: *Self, direction: Direction) *Self {
self.fields.set(1);
pub fn setDirection(self: *Self, direction: Direction) *Self {
self.direction = direction;
return self;
}
pub fn set_method(self: *Self, method: Method) *Self {
self.fields.set(2);
pub fn setMethod(self: *Self, method: Method) *Self {
self.method = method;
return self;
}
pub fn set_application_name(self: *Self, application_name: []const u8) !*Self {
self.fields.set(3);
self.application_name = Bytes.init(self.ally);
try self.application_name.appendSlice(application_name);
return self;
}
pub fn append_aspect(self: *Self, aspect: []const u8) !*Self {
var managed_aspect = Bytes.init(self.ally);
try managed_aspect.appendSlice(aspect);
try self.aspects.append(managed_aspect);
pub fn setName(self: *Self, name: Name) *Self {
self.name = name;
return self;
}
pub fn build(self: *Self) Error!Managed {
errdefer {
self.application_name.clearAndFree();
const direction = self.direction orelse return Error.MissingDirection;
const method = self.method orelse return Error.MissingMethod;
const name = self.name orelse return Error.MissingName;
for (self.aspects.items) |*a| {
a.clearAndFree();
if (method == .plain and self.identity != null) {
return Error.HasIdentity;
} else if (method != .plain and self.identity == null) {
return Error.MissingIdentity;
}
const hash = blk: {
const name_hash = name.hash.name();
if (self.identity) |identity| {
break :blk Hash.ofItems(.{
.name_hash = name_hash,
.identity_hash = identity.hash.short(),
});
} else {
break :blk Hash.ofItems(.{
.name_hash = name_hash,
});
}
};
self.aspects.clearAndFree();
}
if (self.fields.count() == self.fields.capacity()) {
const name_hash = blk: {
// TODO: Do this incrementally without copying.
var name_bytes = Bytes.init(self.ally);
try name_bytes.appendSlice(self.application_name.items);
errdefer {
name_bytes.deinit();
}
for (self.application_name.items) |c| {
if (c == '.') {
return Error.InvalidName;
}
}
for (self.aspects.items) |a| {
for (a.items) |c| {
if (c == '.') {
return Error.InvalidAspect;
}
}
try name_bytes.append('.');
try name_bytes.appendSlice(a.items);
}
break :blk Hash.hash_data(name_bytes.items);
};
const hash = Hash.hash_items(.{
.name_hash = name_hash.name(),
.identity_hash = self.identity.hash.short(),
});
return Managed{
.ally = self.ally,
.identity = self.identity,
.direction = self.direction,
.method = self.method,
.application_name = self.application_name,
.aspects = self.aspects,
.hash = hash,
.name_hash = name_hash,
};
}
return Error.Incomplete;
return Managed{
.ally = self.ally,
.identity = self.identity,
.direction = direction,
.method = method,
.name = name,
.hash = hash,
};
}
pub fn deinit(self: *Self) void {
if (self.name) |name| {
name.deinit();
}
self.* = undefined;
}

View File

@@ -1,44 +1,36 @@
const std = @import("std");
const crypto = @import("../crypto.zig");
const data = @import("../data.zig");
const endpoint = @import("../endpoint.zig");
const Allocator = std.mem.Allocator;
const Bytes = std.ArrayList(u8);
const Identity = crypto.Identity;
const Direction = endpoint.Direction;
const Method = endpoint.Method;
const Name = endpoint.Name;
const Hash = crypto.Hash;
const Self = @This();
ally: Allocator,
identity: Identity,
identity: ?Identity,
direction: Direction,
method: Method,
application_name: Bytes,
aspects: std.ArrayList(Bytes),
name: Name,
hash: Hash,
name_hash: Hash,
pub fn copy(self: *Self) !Self {
var new = Self{
pub fn clone(self: *const Self) !Self {
return Self{
.ally = self.ally,
.identity = self.identity,
.direction = self.direction,
.method = self.method,
.application_name = Bytes.init(self.ally),
.aspects = std.ArrayList(Bytes).init(self.ally),
.name = try self.name.clone(),
.hash = self.hash,
.name_hash = self.name_hash,
};
try new.application_name.appendSlice(self.application_name.items);
for (self.aspects.items) |aspect| {
var new_aspect = Bytes.init(self.ally);
try new_aspect.appendSlice(aspect.items);
try new.aspects.append(new_aspect);
}
return new;
}
pub fn deinit(self: *Self) void {
self.name.deinit();
self.* = undefined;
}

102
core/endpoint/Name.zig Normal file
View File

@@ -0,0 +1,102 @@
const std = @import("std");
const crypto = @import("../crypto.zig");
const data = @import("../data.zig");
const Allocator = std.mem.Allocator;
const Identity = crypto.Identity;
const Hash = crypto.Hash;
const Self = @This();
pub const Error = error{
InvalidName,
InvalidAspect,
};
pub const AppName = data.Bytes;
pub const Aspect = data.Bytes;
pub const Aspects = std.ArrayList(Aspect);
ally: Allocator,
app_name: AppName,
aspects: Aspects,
hash: Hash,
pub fn init(app_name: []const u8, aspects: []const []const u8, ally: Allocator) !Self {
var self = Self{
.ally = ally,
.app_name = .init(ally),
.aspects = .init(ally),
.hash = undefined,
};
errdefer {
self.app_name.deinit();
self.aspects.deinit();
}
for (app_name) |char| {
if (char == '.') {
return Error.InvalidName;
}
}
try self.app_name.appendSlice(app_name);
for (aspects) |aspect| {
for (aspect) |char| {
if (char == '.') {
return Error.InvalidAspect;
}
}
var new_aspect = Aspect.init(self.ally);
errdefer {
new_aspect.deinit();
}
try new_aspect.appendSlice(aspect);
try self.aspects.append(new_aspect);
}
self.hash = blk: {
var name = data.Bytes.init(self.ally);
defer {
name.deinit();
}
try name.appendSlice(app_name);
for (aspects) |aspect| {
try name.append('.');
try name.appendSlice(aspect);
}
break :blk Hash.ofData(name.items);
};
return self;
}
pub fn clone(self: *Self) !Self {
var cloned = self.*;
cloned.app_name = try cloned.app_name.clone();
cloned.aspects = Aspects.init(self.ally);
for (self.aspects) |aspect| {
cloned.aspects.append(try aspect.clone());
}
return cloned;
}
pub fn deinit(self: *Self) void {
self.app_name.deinit();
for (self.aspects.items) |aspect| {
aspect.deinit();
}
self.aspects.deinit();
self.* = undefined;
}

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const crypto = @import("../crypto.zig");
const Allocator = std.mem.Allocator;
const Builder = @import("Builder.zig");
const Endpoint = @import("Managed.zig");
const Identity = crypto.Identity;
const Interface = @import("../Interface.zig");
@@ -12,27 +13,6 @@ const Rng = @import("../System.zig").Rng;
const Self = @This();
ally: Allocator,
entries: std.StringHashMap(Entry),
pub fn init(ally: Allocator) Self {
return .{
.ally = ally,
.entries = std.StringHashMap(Entry).init(ally),
};
}
pub fn deinit(self: *Self) void {
self.entries.deinit();
self.* = undefined;
}
pub fn add(self: *Self, endpoint: Endpoint) Allocator.Error!void {
try self.entries.put(endpoint.hash.bytes[0..], Entry{
.endpoint = endpoint,
});
}
const Entry = struct {
// timestamp: i64,
// expiry_time: i64,
@@ -47,3 +27,45 @@ const Entry = struct {
// origin_announce: ...,
// application_data: []const u8,
};
ally: Allocator,
main: Endpoint,
entries: std.StringHashMap(Entry),
pub fn init(ally: Allocator, main: Endpoint) !Self {
var entries = std.StringHashMap(Entry).init(ally);
try entries.put(main.hash.bytes[0..], Entry{
.endpoint = main,
});
return Self{
.ally = ally,
.main = main,
.entries = entries,
};
}
pub fn getPtr(self: *Self, hash: Hash) ?*const Endpoint {
if (self.entries.get(hash.bytes[0..])) |entry| {
return &entry.endpoint;
}
return null;
}
pub fn add(self: *Self, endpoint: *const Endpoint) !void {
try self.entries.put(endpoint.hash.bytes[0..], Entry{
.endpoint = try endpoint.clone(),
});
}
pub fn deinit(self: *Self) void {
var entries = self.entries.valueIterator();
while (entries.next()) |entry| {
entry.endpoint.deinit();
}
self.entries.deinit();
self.* = undefined;
}

133
core/interface/Manager.zig Normal file
View File

@@ -0,0 +1,133 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Interface = @import("../Interface.zig");
const Packet = @import("../packet/Managed.zig");
const PacketFactory = @import("../packet/Factory.zig");
const System = @import("../System.zig");
const Self = @This();
pub const Error = error{
InterfaceNotFound,
TooManyInterfaces,
} || Allocator.Error;
const Entry = struct {
interface: *Interface,
pending_out: Interface.Outgoing,
};
const interface_limit = 256;
ally: Allocator,
system: System,
entries: std.AutoHashMap(Interface.Id, Entry),
current_interface_id: Interface.Id,
pub fn init(ally: Allocator, system: System) Self {
return Self{
.ally = ally,
.system = system,
.entries = std.AutoHashMap(Interface.Id, Entry).init(ally),
.current_interface_id = 0,
};
}
pub fn iterator(self: *Self) std.AutoHashMap(Interface.Id, Entry).ValueIterator {
return self.entries.valueIterator();
}
pub fn add(self: *Self, config: Interface.Config) Error!Interface.Api {
if (self.entries.count() >= interface_limit) {
return Error.TooManyInterfaces;
}
const id = self.current_interface_id;
self.current_interface_id += 1;
const incoming = try self.ally.create(Interface.Incoming);
incoming.* = Interface.Incoming.init(self.ally);
errdefer {
self.ally.destroy(incoming);
}
const outgoing = try self.ally.create(Interface.Outgoing);
outgoing.* = Interface.Outgoing.init(self.ally);
errdefer {
self.ally.destroy(outgoing);
}
const packet_factory = PacketFactory.init(
self.ally,
self.system.clock,
self.system.rng,
config,
);
const interface = try self.ally.create(Interface);
errdefer {
self.ally.destroy(interface);
}
interface.* = Interface.init(
self.ally,
config,
id,
incoming,
outgoing,
packet_factory,
);
try self.entries.put(id, Entry{
.interface = interface,
.pending_out = Interface.Outgoing.init(self.ally),
});
return interface.api();
}
pub fn remove(self: *Self, id: Interface.Id) void {
if (self.entries.get(id)) |entry| {
entry.interface.deinit(self.ally);
self.ally.destroy(entry.interface);
self.entries.remove(id);
}
}
// TODO: Refactor source_id.
pub fn propagate(self: *Self, packet: Packet, source_id: ?Interface.Id) !void {
var entries = self.entries.valueIterator();
while (entries.next()) |entry| {
if (source_id) |source| {
if (entry.interface.id == source) {
continue;
}
}
try entry.pending_out.push(.{
.packet = try packet.clone(),
});
}
}
pub fn deinit(self: *Self) void {
var entries = self.entries.valueIterator();
while (entries.next()) |entry| {
while (entry.pending_out.pop()) |event| {
var e = event;
e.deinit();
}
entry.pending_out.deinit();
entry.interface.deinit();
self.ally.destroy(entry.interface);
}
self.entries.deinit();
self.* = undefined;
}

View File

@@ -12,16 +12,16 @@ pub fn ThreadSafeFifo(comptime T: type) type {
mutex: std.Thread.Mutex,
impl: Impl,
pub fn init(ally: Allocator) Error!Self {
pub fn init(ally: Allocator) Self {
return Self{
.mutex = .{},
.impl = Impl.init(ally),
};
}
pub fn deinit(self: *Self, ally: Allocator) void {
pub fn deinit(self: *Self) void {
self.mutex.lock();
self.impl.deinit(ally);
self.impl.deinit();
self.mutex.unlock();
self.* = undefined;
}

View File

@@ -1,10 +0,0 @@
const interface = @import("../interface.zig");
const Packet = @import("../packet.zig").Managed;
pub const In = struct {
packet: Packet,
};
pub const Out = struct {
packet: Packet,
};

169
core/node/Event.zig Normal file
View File

@@ -0,0 +1,169 @@
const std = @import("std");
const data = @import("../data.zig");
const endpoint = @import("../endpoint.zig");
const Packet = @import("../packet.zig").Managed;
const Payload = @import("../packet.zig").Payload;
const Hash = @import("../crypto/Hash.zig");
// TODO: Perhaps distinguish between tasks and packets.
pub const In = union(enum) {
announce: Announce,
packet: Packet,
plain: Plain,
pub const Announce = struct {
hash: Hash,
app_data: ?data.Bytes,
};
pub const Plain = struct {
name: endpoint.Name,
payload: Payload,
};
pub fn deinit(self: *@This()) void {
switch (self.*) {
.announce => |*announce| {
if (announce.app_data) |app_data| {
app_data.deinit();
}
},
.packet => |*packet| {
packet.deinit();
},
.plain => |*plain| {
plain.name.deinit();
plain.payload.deinit();
},
}
}
};
pub const Out = union(enum) {
packet: Packet,
pub fn deinit(self: *@This()) void {
switch (self.*) {
.packet => |*packet| {
packet.deinit();
},
}
}
// TODO: Replace this with a cleaner implementation.
pub fn format(this: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, w: anytype) !void {
_ = fmt;
_ = options;
const F = struct {
const Self = @This();
writer: @TypeOf(w),
indentation: u8 = 0,
fn init(writer: @TypeOf(w)) Self {
return .{
.writer = writer,
};
}
fn indent(self: *Self) !void {
for (0..self.indentation) |_| {
try self.writer.print(" ", .{});
}
}
fn entry(self: *Self, key: []const u8, comptime value_fmt: []const u8, args: anytype) !void {
try self.indent();
try self.writer.print(".{s} = ", .{key});
try self.writer.print(value_fmt ++ ",\n", args);
}
fn objectStart(self: *Self, key: []const u8, tag: []const u8) !void {
try self.indent();
try self.writer.print(".{s} = .{s}{{\n", .{ key, tag });
self.indentation += 2;
}
fn objectEnd(self: *Self) !void {
self.indentation -= 2;
try self.indent();
try self.writer.print("}},\n", .{});
}
fn print(self: *Self, comptime text: []const u8, args: anytype) !void {
try self.writer.print(text, args);
}
};
const hex = std.fmt.fmtSliceHexLower;
var f = F.init(w);
switch (this) {
.packet => |p| {
try f.objectStart("packet", "");
const h = p.header;
try f.entry("header", ".{{.{s}, .{s}, .{s}, .{s}, .{s}, .{s}, hops({d})}}", .{
@tagName(h.interface),
@tagName(h.format),
@tagName(h.context),
@tagName(h.propagation),
@tagName(h.method),
@tagName(h.purpose),
h.hops,
});
if (p.interface_access_code.items.len > 0) {
try f.entry("interface_access_code", "{x}", .{hex(p.interface_access_code.items)});
}
switch (p.endpoints) {
.normal => |n| {
try f.entry("endpoints", ".normal{{{x}}}", .{
hex(&n.endpoint),
});
},
.transport => |t| {
try f.entry("endpoints", ".transport{{{x}, {x}}}", .{
hex(&t.endpoint),
hex(&t.transport_id),
});
},
}
try f.entry("context", ".{s}", .{@tagName(p.context)});
switch (p.payload) {
.announce => |a| {
try f.objectStart("payload", "announce");
try f.entry("public.dh", "{x}", .{hex(&a.public.dh)});
try f.entry("public.signature", "{x}", .{hex(&a.public.signature.bytes)});
try f.entry("name_hash", "{x}", .{hex(&a.name_hash)});
try f.entry("noise", "{x}", .{hex(&a.noise)});
try f.entry("timestamp", "{}", .{a.timestamp});
try f.entry("signature", "{x}", .{hex(&a.signature.toBytes())});
if (a.application_data.items.len > 0) {
try f.entry("application_data", "{x}", .{hex(a.application_data.items)});
}
try f.objectEnd();
},
.raw => |r| {
try f.entry("payload", ".raw{{{x}}}", .{hex(r.items)});
},
.none => {
try f.entry("payload", ".none", .{});
},
}
f.indentation -= 2;
try f.indent();
try f.print("}}", .{});
},
}
}
};

View File

@@ -1,6 +1,9 @@
const Identity = @import("../crypto/Identity.zig");
const Self = @This();
// TODO: Load the version from build.zig.zon.
name: []const u8 = "reticulum-zig",
transport_enabled: bool = false,
max_interfaces: usize = 256,
max_incoming_packets: usize = 1024,
max_outgoing_packets: usize = 1024,
incoming_packets_limit: usize = 1024,
outgoing_packets_limit: usize = 1024,

View File

@@ -1,8 +1,8 @@
const std = @import("std");
const crypto = @import("crypto.zig");
const data = @import("data.zig");
const Allocator = std.mem.Allocator;
const Bytes = std.ArrayList(u8);
const Endpoint = @import("endpoint.zig").Managed;
const EndpointMethod = @import("endpoint.zig").Method;
const Hash = crypto.Hash;
@@ -14,6 +14,8 @@ pub const Managed = @import("packet/Managed.zig");
pub const Packet = Managed;
pub const Payload = union(enum) {
const Self = @This();
pub const Announce = struct {
pub const Noise = [5]u8;
pub const Timestamp = u40;
@@ -22,8 +24,8 @@ pub const Payload = union(enum) {
total += crypto.X25519.public_length;
total += crypto.Ed25519.PublicKey.encoded_length;
total += crypto.Hash.name_length;
total += 5;
total += 5;
total += @sizeOf(Noise);
total += @sizeOf(Timestamp);
total += crypto.Ed25519.Signature.encoded_length;
break :blk total;
};
@@ -34,22 +36,47 @@ pub const Payload = union(enum) {
timestamp: Timestamp,
// rachet: ?*const [N]u8,
signature: crypto.Ed25519.Signature,
application_data: Bytes,
application_data: data.Bytes,
};
announce: Announce,
raw: Bytes,
raw: data.Bytes,
none,
pub fn size(self: @This()) usize {
pub fn makeRaw(bytes: data.Bytes) Self {
return Self{
.raw = bytes,
};
}
pub fn clone(self: Self) !Self {
return switch (self) {
.announce => |a| Self{
.announce = Announce{
.public = a.public,
.name_hash = a.name_hash,
.noise = a.noise,
.timestamp = a.timestamp,
.signature = a.signature,
.application_data = try a.application_data.clone(),
},
},
.raw => |r| Self{
.raw = try r.clone(),
},
.none => Self.none,
};
}
pub fn size(self: *const Self) usize {
return switch (self) {
.announce => |*a| blk: {
var total: usize = 0;
total += crypto.X25519.public_length;
total += crypto.Ed25519.PublicKey.encoded_length;
total += crypto.Hash.name_length;
total += 5;
total += 5;
total += @sizeOf(Announce.Noise);
total += @sizeOf(Announce.Timestamp);
total += crypto.Ed25519.Signature.encoded_length;
total += a.application_data.items.len;
break :blk total;
@@ -58,6 +85,14 @@ pub const Payload = union(enum) {
.none => 0,
};
}
pub fn deinit(self: *Self) void {
return switch (self.*) {
.announce => |*announce| announce.application_data.deinit(),
.raw => |*raw| raw.deinit(),
.none => {},
};
}
};
pub const Endpoints = union(Header.Flag.Format) {
@@ -82,7 +117,7 @@ pub const Endpoints = union(Header.Flag.Format) {
};
}
pub fn next_hop(self: Self) Hash.Short {
pub fn nextHop(self: Self) Hash.Short {
return switch (self) {
.normal => |n| n.endpoint,
.transport => |t| t.transport_id,
@@ -132,7 +167,7 @@ pub const Header = packed struct {
};
pub const Context = enum(u8) {
none,
none = 0,
resource,
resource_advertisement,
resource_request,
@@ -155,6 +190,6 @@ pub const Context = enum(u8) {
link_request_proof,
};
test "Header size" {
test "header-size" {
try std.testing.expect(@sizeOf(Header) == 2);
}

View File

@@ -1,11 +1,11 @@
const std = @import("std");
const crypto = @import("../crypto.zig");
const data = @import("../data.zig");
const packet = @import("../packet.zig");
pub const Endpoints = packet.Endpoints;
const Allocator = std.mem.Allocator;
const Bytes = std.ArrayList(u8);
const Managed = @import("Managed.zig");
const Hash = crypto.Hash;
const Header = packet.Header;
@@ -14,54 +14,53 @@ const Payload = packet.Payload;
const Endpoint = @import("../endpoint.zig").Managed;
const Self = @This();
const Fields = std.bit_set.IntegerBitSet(1);
pub const Error = error{Incomplete};
ally: Allocator,
fields: Fields,
header: Header,
interface_access_code: Bytes,
endpoints: Endpoints,
interface_access_code: data.Bytes,
endpoints: ?Endpoints,
context: Context,
payload: Payload,
pub fn init(ally: Allocator) Self {
return Self{
.ally = ally,
.fields = Fields.initEmpty(),
.header = .{},
.interface_access_code = Bytes.init(ally),
.endpoints = undefined,
.interface_access_code = data.Bytes.init(ally),
.endpoints = null,
.context = .none,
.payload = .none,
};
}
pub fn set_header(self: *Self, header: Header) *Self {
pub fn setHeader(self: *Self, header: Header) *Self {
self.header = header;
return self;
}
pub fn set_interface_access_code(self: *Self, interface_access_code: []const u8) !*Self {
pub fn setInterfaceAccessCode(self: *Self, interface_access_code: []const u8) !*Self {
try self.interface_access_code.appendSlice(interface_access_code);
if (interface_access_code.len > 0) {
self.header.interface = .authenticated;
}
return self;
}
pub fn set_endpoint(self: *Self, endpoint_hash: Hash.Short) *Self {
self.fields.set(0);
pub fn setEndpoint(self: *Self, endpoint_hash: Hash.Short) *Self {
self.endpoints = .{
.normal = .{ .endpoint = endpoint_hash },
.normal = .{
.endpoint = endpoint_hash,
},
};
self.header.format = .normal;
return self;
}
pub fn set_transport(self: *Self, endpoint_hash: Hash.Short, transport_id: Hash.Short) *Self {
self.fields.set(0);
pub fn setTransport(self: *Self, endpoint_hash: Hash.Short, transport_id: Hash.Short) *Self {
self.endpoints = .{
.transport = .{
.endpoint = endpoint_hash,
@@ -73,23 +72,23 @@ pub fn set_transport(self: *Self, endpoint_hash: Hash.Short, transport_id: Hash.
return self;
}
pub fn set_method(self: *Self, method: Header.Flag.Method) *Self {
pub fn setMethod(self: *Self, method: Header.Flag.Method) *Self {
self.header.method = method;
return self;
}
pub fn set_purpose(self: *Self, purpose: Header.Flag.Purpose) *Self {
pub fn setPurpose(self: *Self, purpose: Header.Flag.Purpose) *Self {
self.header.purpose = purpose;
return self;
}
pub fn set_context(self: *Self, context: Context) *Self {
pub fn setContext(self: *Self, context: Context) *Self {
self.context = context;
self.header.context = .some;
return self;
}
pub fn set_payload(self: *Self, payload: Payload) *Self {
pub fn setPayload(self: *Self, payload: Payload) *Self {
self.header.purpose = switch (payload) {
.announce => .announce,
else => self.header.purpose,
@@ -98,22 +97,20 @@ pub fn set_payload(self: *Self, payload: Payload) *Self {
return self;
}
pub fn append_payload(self: *Self, payload: []const u8) !*Self {
pub fn appendPayload(self: *Self, payload: []const u8) !*Self {
try self.payload.appendSlice(payload);
return self;
}
pub fn build(self: *Self) !Managed {
if (self.fields.count() == self.fields.capacity()) {
return Managed{
.ally = self.ally,
.header = self.header,
.interface_access_code = self.interface_access_code,
.endpoints = self.endpoints,
.context = self.context,
.payload = self.payload,
};
}
const endpoints = self.endpoints orelse return Error.Incomplete;
return Error.Incomplete;
return Managed{
.ally = self.ally,
.header = self.header,
.interface_access_code = self.interface_access_code,
.endpoints = endpoints,
.context = self.context,
.payload = self.payload,
};
}

View File

@@ -1,13 +1,14 @@
const std = @import("std");
const crypto = @import("../crypto.zig");
const data = @import("../data.zig");
const packet = @import("../packet.zig");
const Allocator = std.mem.Allocator;
const Rng = @import("../System.zig").Rng;
const Clock = @import("../System.zig").Clock;
const Interface = @import("../Interface.zig");
const Bytes = std.ArrayList(u8);
const Endpoint = @import("../endpoint.zig").Managed;
const Name = @import("../endpoint.zig").Name;
const Builder = @import("Builder.zig");
const Packet = @import("Managed.zig");
@@ -16,6 +17,7 @@ const Packet = @import("Managed.zig");
pub const Error = error{
InvalidBytesLength,
InvalidAuthentication,
MissingIdentity,
} || crypto.Identity.Error || Builder.Error || Allocator.Error;
const Self = @This();
@@ -33,7 +35,7 @@ pub fn init(ally: Allocator, clock: Clock, rng: Rng, config: Interface.Config) S
};
}
pub fn from_bytes(self: *Self, bytes: []const u8) Error!Packet {
pub fn fromBytes(self: *Self, bytes: []const u8) Error!Packet {
var index: usize = 0;
const header_size = @sizeOf(packet.Header);
@@ -52,7 +54,7 @@ pub fn from_bytes(self: *Self, bytes: []const u8) Error!Packet {
return Error.InvalidAuthentication;
}
var interface_access_code = Bytes.init(self.ally);
var interface_access_code = data.Bytes.init(self.ally);
errdefer {
interface_access_code.deinit();
}
@@ -62,7 +64,7 @@ pub fn from_bytes(self: *Self, bytes: []const u8) Error!Packet {
return Error.InvalidBytesLength;
}
// I need to decrypt the packet here.
// TODO: I need to decrypt the packet here.
try interface_access_code.appendSlice(bytes[index .. index + access_code.len]);
index += access_code.len;
@@ -141,14 +143,14 @@ pub fn from_bytes(self: *Self, bytes: []const u8) Error!Packet {
announce.signature = Signature.fromBytes(signature_bytes);
index += Signature.encoded_length;
var application_data = Bytes.init(self.ally);
var application_data = data.Bytes.init(self.ally);
try application_data.appendSlice(bytes[index..]);
announce.application_data = application_data;
break :blk announce;
} },
else => .{ .raw = blk: {
var raw = Bytes.init(self.ally);
var raw = data.Bytes.init(self.ally);
errdefer {
raw.deinit();
}
@@ -168,21 +170,22 @@ pub fn from_bytes(self: *Self, bytes: []const u8) Error!Packet {
}
// TODO: Add ratchet.
pub fn make_announce(self: *Self, endpoint: *const Endpoint, application_data: ?[]const u8) Error!Packet {
pub fn makeAnnounce(self: *Self, endpoint: *const Endpoint, application_data: ?[]const u8) Error!Packet {
const identity = endpoint.identity orelse return Error.MissingIdentity;
var announce: packet.Payload.Announce = undefined;
announce.public = endpoint.identity.public;
announce.name_hash = endpoint.name_hash.name().*;
announce.public = identity.public;
announce.name_hash = endpoint.name.hash.name().*;
self.rng.bytes(&announce.noise);
announce.timestamp = @truncate(std.mem.nativeToBig(u64, self.clock.monotonicMicros()));
announce.application_data = Bytes.init(self.ally);
announce.application_data = data.Bytes.init(self.ally);
if (application_data) |data| {
try announce.application_data.appendSlice(data);
if (application_data) |app_data| {
try announce.application_data.appendSlice(app_data);
}
announce.signature = blk: {
var signer = try endpoint.identity.signer(&self.rng);
var signer = try identity.signer(&self.rng);
signer.update(endpoint.hash.short());
signer.update(announce.public.dh[0..]);
signer.update(announce.public.signature.bytes[0..]);
@@ -196,11 +199,27 @@ pub fn make_announce(self: *Self, endpoint: *const Endpoint, application_data: ?
var builder = Builder.init(self.ally);
if (self.config.access_code) |interface_access_code| {
_ = try builder.set_interface_access_code(interface_access_code);
_ = try builder.setInterfaceAccessCode(interface_access_code);
}
return try builder
.set_endpoint(endpoint.hash.short().*)
.set_payload(.{ .announce = announce })
.setEndpoint(endpoint.hash.short().*)
.setPayload(.{ .announce = announce })
.build();
}
pub fn makePlain(self: *Self, name: Name, payload: packet.Payload) Error!Packet {
var builder = Builder.init(self.ally);
if (self.config.access_code) |interface_access_code| {
_ = try builder.setInterfaceAccessCode(interface_access_code);
}
const plain = try builder
.setMethod(.plain)
.setEndpoint(name.hash.short().*)
.setPayload(payload)
.build();
return plain;
}

View File

@@ -1,9 +1,9 @@
const std = @import("std");
const crypto = @import("../crypto.zig");
const data = @import("../data.zig");
const packet = @import("../packet.zig");
const Allocator = std.mem.Allocator;
const Bytes = std.ArrayList(u8);
const Header = packet.Header;
const Context = packet.Context;
const Endpoints = packet.Endpoints;
@@ -13,25 +13,39 @@ const Hash = crypto.Hash;
const Self = @This();
ally: Allocator,
header: Header = undefined,
interface_access_code: Bytes,
endpoints: Endpoints = undefined,
context: Context = undefined,
header: Header,
interface_access_code: data.Bytes,
endpoints: Endpoints,
context: Context,
payload: Payload,
pub fn init(ally: Allocator) Self {
return Self{
.ally = ally,
.interface_access_code = Bytes.init(ally),
.header = undefined,
.interface_access_code = data.Bytes.init(ally),
.endpoints = undefined,
.context = undefined,
.payload = .none,
};
}
pub fn deinit(self: *Self) Self {
_ = self;
pub fn deinit(self: *Self) void {
self.interface_access_code.deinit();
self.payload.deinit();
}
pub fn validate(self: *Self) !bool {
pub fn setTransport(self: *Self, transport_id: *const Hash.Short) !void {
self.header.format = .transport;
self.endpoints = Endpoints{
.transport = Endpoints.Transport{
.transport_id = transport_id.*,
.endpoint = self.endpoints.endpoint(),
},
};
}
pub fn validate(self: *const Self) !void {
switch (self.payload) {
.announce => |a| {
const endpoint_hash = self.endpoints.endpoint();
@@ -46,20 +60,22 @@ pub fn validate(self: *Self) !bool {
try verifier.verify();
const identity = crypto.Identity.from_public(a.public);
const expected_hash = Hash.hash_items(.{
const expected_hash = Hash.ofItems(.{
.name_hash = a.name_hash,
.public_hash = identity.hash.short(),
});
const matching_hashes = std.mem.eql(u8, endpoint_hash[0..], expected_hash.short()[0..]);
return matching_hashes;
if (!matching_hashes) {
return error.MismatchingHashes;
}
},
else => return true,
else => return,
}
}
// TODO: Make this take a Writer interface.
// Make sure to encrypt the packet with the interface access code here.
// TODO: Make sure to encrypt the packet with the interface access code here.
pub fn write(self: *const Self, buffer: []u8) []u8 {
if (buffer.len < self.size()) {
return &.{};
@@ -123,13 +139,13 @@ pub fn hash(self: *const Self) Hash {
const header: u8 = std.mem.bytesAsSlice(u4, self.header)[1];
return switch (self.endpoints) {
.normal => |normal| Hash.from_items(.{
.normal => |normal| Hash.fromItems(.{
.header = header,
.endpoint = normal.endpoint,
.context = self.context,
.payload = self.payload,
}),
.transport => |transport| Hash.from_items(.{
.transport => |transport| Hash.fromItems(.{
.header = header,
.transport_id = transport.transport_id,
.endpoint = transport.endpoint,
@@ -153,3 +169,14 @@ pub fn size(self: *const Self) usize {
return total_size;
}
pub fn clone(self: *const Self) !Self {
return Self{
.ally = self.ally,
.context = self.context,
.endpoints = self.endpoints,
.header = self.header,
.interface_access_code = try self.interface_access_code.clone(),
.payload = try self.payload.clone(),
};
}

View File

@@ -1,7 +1,8 @@
pub const crypto = @import("crypto.zig");
pub const data = @import("data.zig");
pub const endpoint = @import("endpoint.zig");
pub const packet = @import("packet.zig");
pub const units = @import("units.zig");
pub const unit = @import("unit.zig");
pub const Endpoint = endpoint.Managed;
pub const Identity = crypto.Identity;

View File

@@ -1,6 +1,7 @@
pub const Bandwidth = BitRate;
pub const BitRate = struct {
pub const none: ?BitRate = null;
pub const default = BitRate{
.bits = .{ .count = 1, .prefix = .kilo },
.rate = .per_second,

View File

@@ -1,2 +0,0 @@
pub const Clock = @import("fixtures/Clock.zig");
pub const Framework = @import("fixtures/Framework.zig");

View File

@@ -1,124 +0,0 @@
const std = @import("std");
const rt = @import("reticulum");
const Allocator = std.mem.Allocator;
const Clock = @import("Clock.zig");
pub const Error = error{
UnknownName,
DuplicateName,
} || Allocator.Error;
const Node = struct {
node: rt.Node,
endpoints: std.ArrayList(rt.Endpoint),
api: rt.Interface.Api,
};
const Self = @This();
ally: Allocator,
options: rt.Node.Options,
clock: Clock,
system: rt.System,
indices: std.StringHashMap(usize),
edges: std.ArrayList(std.AutoHashMap(usize, void)),
nodes: std.ArrayList(Node),
pub fn init(ally: Allocator, options: rt.Node.Options) Self {
var clock = Clock.init();
return Self{
.ally = ally,
.options = options,
.clock = clock,
.system = rt.System{
.clock = clock.clock(),
.rng = std.crypto.random,
},
.indices = std.StringHashMap(usize).init(ally),
.edges = std.ArrayList(std.AutoHashMap(usize, void)).init(ally),
.nodes = std.ArrayList(Node).init(ally),
};
}
pub fn addEndpoint(self: *Self, name: []const u8) !rt.Endpoint {
const identity = try rt.Identity.random(&self.system.rng);
var builder = rt.endpoint.Builder.init(self.ally);
_ = try builder
.set_identity(identity)
.set_direction(.in)
.set_method(.single)
.set_application_name(name);
var endpoint = try builder.build();
if (self.indices.get(name)) |index| {
var node = self.nodes.items[index];
const endpoint_copy = try endpoint.copy();
try node.endpoints.append(endpoint_copy);
}
return endpoint;
}
pub fn getNode(self: *Self, name: []const u8) ?*Node {
const index = self.indices.get(name) orelse return null;
return &self.nodes.items[index];
}
pub fn send(self: *Self, src: []const u8, dst: []const u8, data: []const u8) !void {
const n1 = self.getNode(src) orelse return;
const n2 = self.getNode(dst) orelse return;
const e1 = n1.endpoints.getLast();
const e2 = n2.endpoints.getLast();
const packet = rt.packet.Builder.init(self.ally)
.set_transport(e1.hash, e2.hash)
.append_payload(data)
.build();
try n1.api.send(packet);
}
pub fn process(self: *Self) !void {
var indices = self.indices.valueIterator();
while (indices.next()) |index| {
var target = self.nodes.items[index.*];
try target.node.process();
while (target.api.collect(rt.units.BitRate.default)) |packet| {
var keys = self.edges.items[index.*].keyIterator();
while (keys.next()) |k| {
var connected = self.nodes.items[k.*];
try connected.api.deliver(packet);
try connected.node.process();
}
}
}
}
pub fn addNode(self: *Self, name: []const u8) !void {
if (self.indices.contains(name)) {
return Error.DuplicateName;
}
const index = self.indices.count();
try self.indices.put(name, index);
try self.edges.append(std.AutoHashMap(usize, void).init(self.ally));
var node = try rt.Node.init(self.ally, self.system, self.options);
const api = try node.addInterface(.{});
try self.nodes.append(Node{
.node = node,
.endpoints = std.ArrayList(rt.Endpoint).init(self.ally),
.api = api,
});
}
pub fn connect(self: *Self, a: []const u8, b: []const u8) !void {
const a_index = self.indices.get(a) orelse return Error.UnknownName;
const b_index = self.indices.get(b) orelse return Error.UnknownName;
try self.edges.items[a_index].put(b_index, {});
try self.edges.items[b_index].put(a_index, {});
}

View File

@@ -1,40 +0,0 @@
const std = @import("std");
const rt = @import("reticulum");
const fixtures = @import("fixtures");
// const ohsnap = @import("ohsnap");
test {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var thread_safe_gpa = std.heap.ThreadSafeAllocator{
.child_allocator = gpa.allocator(),
};
const ally = thread_safe_gpa.allocator();
var f = try abc(ally);
var c = f.getNode("C").?;
while (c.api.collect(rt.units.BitRate.default)) |packet| {
std.debug.print("{any}\n", .{packet});
}
}
fn abc(ally: std.mem.Allocator) !fixtures.Framework {
var f = fixtures.Framework.init(ally, .{});
const names = [_][]const u8{ "A", "B", "C" };
for (names) |name| {
try f.addNode(name);
}
try f.connect("A", "B");
try f.connect("C", "B");
for (names) |name| {
const n = f.getNode(name).?;
const endpoint = try f.addEndpoint(name);
try n.api.announce(&endpoint, name);
f.clock.advance(200, .ms);
try f.process();
}
return f;
}

View File

@@ -1,6 +1,10 @@
const rt = @import("reticulum");
pub const Unit = enum { s, ms, us, ns };
pub const Unit = enum {
seconds,
milliseconds,
microseconds,
};
const Self = @This();
@@ -14,12 +18,11 @@ pub fn init() Self {
pub fn advance(self: *Self, count: u64, unit: Unit) void {
const factor: u64 = switch (unit) {
.s => 1,
.s => 1_000_000,
.ms => 1_000,
.us => 1_000_000,
.ns => 1_000_000_000,
.us => 1,
};
self.timestamp += (factor * count);
self.timestamp += (count * factor);
}
pub fn monotonicMicros(ptr: *anyopaque) u64 {

View File

@@ -0,0 +1,231 @@
const std = @import("std");
const rt = @import("reticulum");
const Allocator = std.mem.Allocator;
const ManualClock = @import("ManualClock.zig");
pub const Error = error{
UnknownName,
DuplicateName,
} || Allocator.Error;
const Interface = struct {
api: rt.Interface.Api,
config: rt.Interface.Config,
to: []const u8,
event_buffer: std.ArrayList(rt.Node.Event.Out),
};
const Node = struct {
node: rt.Node,
interfaces: std.StringHashMap(void),
};
// Could potentially make this a compile time function instead of a struct.
// TODO: Refactor.
const Self = @This();
ally: Allocator,
manual_clock: ManualClock,
prng: std.Random.DefaultPrng,
system: rt.System,
nodes: std.StringHashMap(Node),
interfaces: std.StringHashMap(Interface),
pub fn init(ally: Allocator) Self {
var manual_clock = ManualClock.init();
return Self{
.ally = ally,
.manual_clock = manual_clock,
.system = rt.System{
.clock = manual_clock.clock(),
.rng = std.crypto.random,
},
.nodes = std.StringHashMap(Node).init(ally),
.interfaces = std.StringHashMap(Interface).init(ally),
};
}
pub fn deinit(self: *Self) void {
var nodes = self.nodes.valueIterator();
var interfaces = self.interfaces.valueIterator();
while (nodes.next()) |node| {
node.node.deinit();
node.interfaces.deinit();
}
while (interfaces.next()) |interface| {
for (interface.event_buffer.items) |*event| {
event.deinit();
}
interface.event_buffer.deinit();
}
self.nodes.deinit();
self.interfaces.deinit();
self.* = undefined;
}
pub fn fromTopology(comptime topology: anytype, ally: Allocator) !Self {
var manual_clock = ManualClock.init();
var prng = std.Random.DefaultPrng.init(42);
var self = Self{
.ally = ally,
.manual_clock = manual_clock,
.prng = prng,
.system = rt.System{
.clock = manual_clock.clock(),
.rng = prng.random(),
},
.nodes = std.StringHashMap(Node).init(ally),
.interfaces = std.StringHashMap(Interface).init(ally),
};
inline for (std.meta.fields(@TypeOf(topology))) |node_field| {
const node = @field(topology, node_field.name);
// TODO: Allow for specifying some fields and defaulting others.
var node_options: rt.Node.Options = .{};
if (@hasField(@TypeOf(node), "options")) {
const topo_options = node.options;
if (@hasField(@TypeOf(topo_options), "transport_enabled")) {
node_options.transport_enabled = topo_options.transport_enabled;
}
if (@hasField(@TypeOf(topo_options), "incoming_packets_limit")) {
node_options.incoming_packets_limit = topo_options.incoming_packets_limit;
}
if (@hasField(@TypeOf(topo_options), "outgoing_packets_limit")) {
node_options.outgoing_packets_limit = topo_options.outgoing_packets_limit;
}
if (@hasField(@TypeOf(topo_options), "name")) {
node_options.name = topo_options.name;
}
}
node_options.name = node_field.name;
const simulator_node = try self.addNode(node_field.name, node_options);
inline for (std.meta.fields(@TypeOf(node.interfaces))) |interface_field| {
const interface = @field(node.interfaces, interface_field.name);
// TODO: Allow for specifying some fields and defaulting others.
var interface_config = rt.Interface.Config{};
if (@hasField(@TypeOf(interface), "config")) {
interface_config = interface.config;
}
interface_config.name = interface_field.name;
const simulator_interface = Interface{
.api = try simulator_node.node.addInterface(interface_config),
.config = interface_config,
.to = @tagName(interface.to),
.event_buffer = std.ArrayList(rt.Node.Event.Out).init(self.ally),
};
try simulator_node.interfaces.put(interface_field.name, {});
try self.interfaces.put(interface_field.name, simulator_interface);
}
}
return self;
}
pub fn addNode(self: *Self, name: []const u8, options: rt.Node.Options) !*Node {
if (self.nodes.contains(name)) {
return Error.DuplicateName;
}
const node = Node{
.node = try rt.Node.init(self.ally, &self.system, null, options),
.interfaces = std.StringHashMap(void).init(self.ally),
};
try self.nodes.put(name, node);
return self.nodes.getPtr(name).?;
}
pub fn addEndpoint(self: *Self, name: []const u8) !rt.Endpoint {
const identity = try rt.Identity.random(&self.system.rng);
var builder = rt.endpoint.Builder.init(self.ally);
_ = try builder
.setIdentity(identity)
.setDirection(.in)
.setMethod(.single)
.setApplicationName(name);
var endpoint = try builder.build();
if (self.indices.get(name)) |index| {
var node = self.nodes.items[index];
const endpoint_copy = try endpoint.clone();
try node.endpoints.append(endpoint_copy);
}
return endpoint;
}
pub fn getNode(self: *Self, name: []const u8) ?*Node {
return self.nodes.getPtr(name);
}
pub fn getInterface(self: *Self, name: []const u8) ?*Interface {
return self.interfaces.getPtr(name);
}
pub fn step(self: *Self) !void {
try self.processBuffers();
try self.processNodes();
}
pub fn stepAfter(self: *Self, count: u64, unit: ManualClock.Unit) !void {
self.manual_clock.advance(count, unit);
try self.step();
}
pub fn processBuffers(self: *Self) !void {
var node_names = self.nodes.keyIterator();
while (node_names.next()) |node_name| {
const node = self.getNode(node_name.*).?;
var interface_names = node.interfaces.keyIterator();
while (interface_names.next()) |interface_name| {
var source_interface = self.interfaces.getPtr(interface_name.*).?;
const target_interface = self.getInterface(source_interface.to).?;
for (source_interface.event_buffer.items) |event_out| {
if (event_out == .packet) {
try target_interface.api.deliverEvent(rt.Node.Event.In{
.packet = event_out.packet,
});
}
}
source_interface.event_buffer.clearRetainingCapacity();
}
}
}
pub fn processNodes(self: *Self) !void {
var node_names = self.nodes.keyIterator();
while (node_names.next()) |node_name| {
const node = self.getNode(node_name.*).?;
try node.node.process();
var interface_names = node.interfaces.keyIterator();
while (interface_names.next()) |interface_name| {
var interface = self.interfaces.getPtr(interface_name.*).?;
while (interface.api.collectEvent()) |event_out| {
try interface.event_buffer.append(event_out);
}
}
}
}

View File

@@ -0,0 +1,27 @@
//! ┌────────┐ ┌─────────┐ ┌────────┐
//! │ a0───┼────┼─b0[T]b1─┼────┼───c0 │
//! └────────┘ └─────────┘ └────────┘
pub const abc = .{
.a = .{
.interfaces = .{
.a0 = .{ .to = .b0 },
},
},
.b = .{
.options = .{
.transport_enabled = true,
.interface_limit = 256,
.incoming_packets_limit = 1024,
.outgoing_packets_limit = 1024,
},
.interfaces = .{
.b0 = .{ .to = .a0 },
.b1 = .{ .to = .c0 },
},
},
.c = .{
.interfaces = .{
.c0 = .{ .to = .b1 },
},
},
};

54
test/simulation/plain.zig Normal file
View File

@@ -0,0 +1,54 @@
const std = @import("std");
const rt = @import("reticulum");
const t = std.testing;
const topology = @import("kit/topology.zig");
const Golden = @import("ohsnap");
const Simulator = @import("kit/Simulator.zig");
test "abc" {
const golden = Golden{};
const ally = t.allocator;
var s = try Simulator.fromTopology(topology.abc, ally);
defer {
s.deinit();
}
const a0 = s.getInterface("a0").?;
const b0 = s.getInterface("b0").?;
const b1 = s.getInterface("b1").?;
const c0 = s.getInterface("c0").?;
const name = try rt.endpoint.Name.init(
"plain",
&.{ "test", "endpoint" },
ally,
);
const payload = rt.packet.Payload.makeRaw(try rt.data.makeBytes(
"this is some payload data",
ally,
));
try a0.api.plain(name, payload);
try s.step();
try t.expect(a0.event_buffer.items.len == 1);
inline for (&.{ b0, b1, c0 }) |n| {
try t.expect(n.event_buffer.items.len == 0);
}
const plain_packet = a0.event_buffer.items[0];
try golden.snap(
@src(),
\\.packet = .{
\\ .header = .{.open, .normal, .none, .broadcast, .plain, .data, hops(0)},
\\ .endpoints = .normal{358602dccbe1449748d6ef6b0c1b0471},
\\ .context = .none,
\\ .payload = .raw{7468697320697320736f6d65207061796c6f61642064617461},
\\}
,
).expectEqualFmt(plain_packet);
}