diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8a308df
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+# zaprus
+
+This is an implementation of the [Saprus protocol](https://gitlab.com/c2-games/red-team/saprus) in Zig.
+It is useful for developing clients either in Zig, or in any other language using the C bindings.
+
+Binary releases can be downloaded [here](https://cloud.zambito.xyz/s/cNaLeDz38W5ZcZs).
diff --git a/build.zig b/build.zig
index 1b7e474..440917f 100644
--- a/build.zig
+++ b/build.zig
@@ -21,6 +21,9 @@ pub fn build(b: *std.Build) void {
// target and optimize options) will be listed when running `zig build --help`
// in this directory.
+ // Get default install step (called with `zig build` or `zig build install`)
+ const install_step = b.getInstallStep();
+
// This creates a module, which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries.
// Zig modules are the preferred way of making Zig code available to consumers.
@@ -41,6 +44,22 @@ pub fn build(b: *std.Build) void {
.target = target,
});
+ // Only used to generate the documentation
+ const zaprus_lib = b.addLibrary(.{
+ .name = "zaprus",
+ .root_module = mod,
+ });
+
+ const docs_step = b.step("doc", "Emit documentation");
+ const docs_install = b.addInstallDirectory(.{
+ .install_dir = .prefix,
+ .install_subdir = "docs",
+ .source_dir = zaprus_lib.getEmittedDocs(),
+ });
+
+ docs_step.dependOn(&docs_install.step);
+ install_step.dependOn(docs_step);
+
// Create static library
const lib = b.addLibrary(.{
.name = "zaprus",
diff --git a/src/Client.zig b/src/Client.zig
index 1709cab..2344f83 100644
--- a/src/Client.zig
+++ b/src/Client.zig
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License along with
// Zaprus. If not, see .
+//! A client is used to handle interactions with the network.
+
const base64_enc = std.base64.standard.Encoder;
const base64_dec = std.base64.standard.Decoder;
@@ -37,6 +39,8 @@ pub fn deinit(self: *Client) void {
self.* = undefined;
}
+/// Sends a fire and forget message over the network.
+/// This function asserts that `payload` fits within a single packet.
pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void {
const io_source: std.Random.IoSource = .{ .io = io };
const rand = io_source.interface();
@@ -76,7 +80,8 @@ pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void
try self.socket.send(full_msg);
}
-pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
+/// Attempts to establish a new connection with the sentinel.
+pub fn connect(self: Client, io: Io, payload: []const u8) (error{ BpfAttachFailed, Timeout } || SaprusMessage.ParseError)!SaprusConnection {
const io_source: std.Random.IoSource = .{ .io = io };
const rand = io_source.interface();
@@ -157,13 +162,17 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
try self.socket.send(full_msg);
- return .init(self.socket, headers, connection);
+ return .{
+ .socket = self.socket,
+ .headers = headers,
+ .connection = connection,
+ };
}
const RawSocket = @import("./RawSocket.zig");
const SaprusMessage = @import("message.zig").Message;
-const saprusParse = @import("message.zig").parse;
+const saprusParse = SaprusMessage.parse;
const SaprusConnection = @import("Connection.zig");
const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp;
diff --git a/src/Connection.zig b/src/Connection.zig
index bb81c38..19be710 100644
--- a/src/Connection.zig
+++ b/src/Connection.zig
@@ -20,17 +20,16 @@ connection: SaprusMessage,
const Connection = @This();
-pub fn init(socket: RawSocket, headers: EthIpUdp, connection: SaprusMessage) Connection {
- return .{
- .socket = socket,
- .headers = headers,
- .connection = connection,
- };
-}
-
// 'p' as base64
const pong = "cA==";
+/// Attempts to read from the network, and returns the next message, if any.
+///
+/// Asserts that `buf` is large enough to store the message that is received.
+///
+/// This will internally process management messages, and return the message
+/// payload for the next non management connection message.
+/// This function is ignorant to the message encoding.
pub fn next(self: *Connection, io: Io, buf: []u8) ![]const u8 {
while (true) {
log.debug("Awaiting connection message", .{});
@@ -65,6 +64,10 @@ pub fn next(self: *Connection, io: Io, buf: []u8) ![]const u8 {
}
}
+/// Attempts to write a message to the network.
+///
+/// Clients should pass `.{}` for options unless you know what you are doing.
+/// `buf` will be sent over the network as-is; this function is ignorant of encoding.
pub fn send(self: *Connection, io: Io, options: SaprusMessage.Connection.Options, buf: []const u8) !void {
const io_source: std.Random.IoSource = .{ .io = io };
const rand = io_source.interface();
diff --git a/src/RawSocket.zig b/src/RawSocket.zig
index e43a8e4..9561dcf 100644
--- a/src/RawSocket.zig
+++ b/src/RawSocket.zig
@@ -32,7 +32,12 @@ const Ifconf = extern struct {
},
};
-pub fn init() !RawSocket {
+pub fn init() error{
+ SocketError,
+ NicError,
+ NoInterfaceFound,
+ BindError,
+}!RawSocket {
const socket: i32 = std.math.cast(i32, std.os.linux.socket(std.os.linux.AF.PACKET, std.os.linux.SOCK.RAW, 0)) orelse return error.SocketError;
if (socket < 0) return error.SocketError;
diff --git a/src/message.zig b/src/message.zig
index 0c1410d..4198737 100644
--- a/src/message.zig
+++ b/src/message.zig
@@ -14,230 +14,219 @@
// You should have received a copy of the GNU General Public License along with
// Zaprus. If not, see .
-pub const MessageTypeError = error{
- NotImplementedSaprusType,
- UnknownSaprusType,
-};
-pub const MessageParseError = MessageTypeError || error{
- InvalidMessage,
-};
-
-const message = @This();
-
pub const Message = union(enum(u16)) {
relay: Message.Relay = 0x003C,
connection: Message.Connection = 0x00E9,
_,
- pub const Relay = message.Relay;
- pub const Connection = message.Connection;
+ pub const Relay = struct {
+ dest: Dest,
+ checksum: [2]u8 = undefined,
+ payload: []const u8,
- pub fn toBytes(self: message.Message, buf: []u8) []u8 {
+ pub const Dest = struct {
+ bytes: [relay_dest_len]u8,
+
+ /// Asserts bytes is less than or equal to 4 bytes
+ pub fn fromBytes(bytes: []const u8) Dest {
+ var buf: [4]u8 = @splat(0);
+ std.debug.assert(bytes.len <= buf.len);
+ @memcpy(buf[0..bytes.len], bytes);
+ return .{ .bytes = buf };
+ }
+ };
+
+ /// Asserts that buf is large enough to fit the relay message.
+ pub fn toBytes(self: Relay, buf: []u8) []u8 {
+ var out: Writer = .fixed(buf);
+ out.writeInt(u16, @intFromEnum(Message.relay), .big) catch unreachable;
+ out.writeInt(u16, @intCast(self.payload.len + 4), .big) catch unreachable; // Length field, but unread. Will switch to checksum
+ out.writeAll(&self.dest.bytes) catch unreachable;
+ out.writeAll(self.payload) catch unreachable;
+ return out.buffered();
+ }
+
+ // test toBytes {
+ // var buf: [1024]u8 = undefined;
+ // const relay: Relay = .init(
+ // .fromBytes(&.{ 172, 18, 1, 30 }),
+ // // zig fmt: off
+ // &[_]u8{
+ // 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65,
+ // 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64
+ // },
+ // // zig fmt: on
+ // );
+ // // zig fmt: off
+ // var expected = [_]u8{
+ // 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72,
+ // 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65,
+ // 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64
+ // };
+ // // zig fmt: on
+ // try expectEqualMessageBuffers(&expected, relay.toBytes(&buf));
+ // }
+ };
+
+ pub const Connection = struct {
+ src: u16,
+ dest: u16,
+ seq: u32,
+ id: u32,
+ reserved: u8 = undefined,
+ options: Options = .{},
+ payload: []const u8,
+
+ /// Option values.
+ /// Currently used!
+ pub const Options = packed struct(u8) {
+ opt1: bool = false,
+ opt2: bool = false,
+ opt3: bool = false,
+ opt4: bool = false,
+ opt5: bool = false,
+ opt6: bool = false,
+ opt7: bool = false,
+ management: bool = false,
+ };
+
+ /// Asserts that buf is large enough to fit the connection message.
+ pub fn toBytes(self: Connection, buf: []u8) []u8 {
+ var out: Writer = .fixed(buf);
+ out.writeInt(u16, @intFromEnum(Message.connection), .big) catch unreachable;
+ out.writeInt(u16, @intCast(self.payload.len + 14), .big) catch unreachable; // Saprus length field, unread.
+ out.writeInt(u16, self.src, .big) catch unreachable;
+ out.writeInt(u16, self.dest, .big) catch unreachable;
+ out.writeInt(u32, self.seq, .big) catch unreachable;
+ out.writeInt(u32, self.id, .big) catch unreachable;
+ out.writeByte(self.reserved) catch unreachable;
+ out.writeStruct(self.options, .big) catch unreachable;
+ out.writeAll(self.payload) catch unreachable;
+ return out.buffered();
+ }
+
+ /// If the current message is a management message, return what kind.
+ /// Else return null.
+ pub fn management(self: Connection) ParseError!?Management {
+ const b64_dec = std.base64.standard.Decoder;
+ if (self.options.management) {
+ var buf: [1]u8 = undefined;
+ _ = b64_dec.decode(&buf, self.payload) catch return error.InvalidMessage;
+
+ return switch (buf[0]) {
+ 'P' => .ping,
+ 'p' => .pong,
+ else => error.UnknownSaprusType,
+ };
+ }
+ return null;
+ }
+
+ pub const Management = enum {
+ ping,
+ pong,
+ };
+ };
+
+ pub fn toBytes(self: Message, buf: []u8) []u8 {
return switch (self) {
inline .relay, .connection => |m| m.toBytes(buf),
else => unreachable,
};
}
- pub const parse = message.parse;
-};
+ pub fn parse(bytes: []const u8) ParseError!Message {
+ var in: Reader = .fixed(bytes);
+ const @"type" = in.takeEnum(std.meta.Tag(Message), .big) catch |err| switch (err) {
+ error.InvalidEnumTag => return error.UnknownSaprusType,
+ else => return error.InvalidMessage,
+ };
+ const checksum = in.takeArray(2) catch return error.InvalidMessage;
+ switch (@"type") {
+ .relay => {
+ const dest: Relay.Dest = .fromBytes(
+ in.takeArray(relay_dest_len) catch return error.InvalidMessage,
+ );
+ const payload = in.buffered();
+ return .{
+ .relay = .{
+ .dest = dest,
+ .checksum = checksum.*,
+ .payload = payload,
+ },
+ };
+ },
+ .connection => {
+ const src = in.takeInt(u16, .big) catch return error.InvalidMessage;
+ const dest = in.takeInt(u16, .big) catch return error.InvalidMessage;
+ const seq = in.takeInt(u32, .big) catch return error.InvalidMessage;
+ const id = in.takeInt(u32, .big) catch return error.InvalidMessage;
+ const reserved = in.takeByte() catch return error.InvalidMessage;
+ const options = in.takeStruct(Connection.Options, .big) catch return error.InvalidMessage;
+ const payload = in.buffered();
+ return .{
+ .connection = .{
+ .src = src,
+ .dest = dest,
+ .seq = seq,
+ .id = id,
+ .reserved = reserved,
+ .options = options,
+ .payload = payload,
+ },
+ };
+ },
+ else => return error.NotImplementedSaprusType,
+ }
+ }
-pub const relay_dest_len = 4;
+ test parse {
+ _ = try parse(&[_]u8{ 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 });
-pub fn parse(bytes: []const u8) MessageParseError!Message {
- var in: Reader = .fixed(bytes);
- const @"type" = in.takeEnum(std.meta.Tag(Message), .big) catch |err| switch (err) {
- error.InvalidEnumTag => return error.UnknownSaprusType,
- else => return error.InvalidMessage,
- };
- const checksum = in.takeArray(2) catch return error.InvalidMessage;
- switch (@"type") {
- .relay => {
- const dest: Relay.Dest = .fromBytes(
- in.takeArray(relay_dest_len) catch return error.InvalidMessage,
- );
- const payload = in.buffered();
- return .{
- .relay = .{
- .dest = dest,
- .checksum = checksum.*,
- .payload = payload,
- },
- };
- },
- .connection => {
- const src = in.takeInt(u16, .big) catch return error.InvalidMessage;
- const dest = in.takeInt(u16, .big) catch return error.InvalidMessage;
- const seq = in.takeInt(u32, .big) catch return error.InvalidMessage;
- const id = in.takeInt(u32, .big) catch return error.InvalidMessage;
- const reserved = in.takeByte() catch return error.InvalidMessage;
- const options = in.takeStruct(Connection.Options, .big) catch return error.InvalidMessage;
- const payload = in.buffered();
- return .{
+ {
+ const expected: Message = .{
.connection = .{
- .src = src,
- .dest = dest,
- .seq = seq,
- .id = id,
- .reserved = reserved,
- .options = options,
- .payload = payload,
+ .src = 12416,
+ .dest = 61680,
+ .seq = 0,
+ .id = 0,
+ .reserved = 0,
+ .options = @bitCast(@as(u8, 100)),
+ .payload = &[_]u8{ 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 },
},
};
- },
- else => return error.NotImplementedSaprusType,
- }
-}
+ const actual = try parse(&[_]u8{ 0x00, 0xe9, 0x00, 0x18, 0x30, 0x80, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 });
-test parse {
- _ = try parse(&[_]u8{ 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 });
-
- {
- const expected: Message = .{
- .connection = .{
- .src = 12416,
- .dest = 61680,
- .seq = 0,
- .id = 0,
- .reserved = 0,
- .options = @bitCast(@as(u8, 100)),
- .payload = &[_]u8{ 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 },
- },
- };
- const actual = try parse(&[_]u8{ 0x00, 0xe9, 0x00, 0x18, 0x30, 0x80, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 });
-
- try std.testing.expectEqualDeep(expected, actual);
- }
-}
-
-const Relay = struct {
- dest: Dest,
- checksum: [2]u8 = undefined,
- payload: []const u8,
-
- pub const Dest = struct {
- bytes: [relay_dest_len]u8,
-
- /// Asserts bytes is less than or equal to 4 bytes
- pub fn fromBytes(bytes: []const u8) Dest {
- var buf: [4]u8 = @splat(0);
- std.debug.assert(bytes.len <= buf.len);
- @memcpy(buf[0..bytes.len], bytes);
- return .{ .bytes = buf };
+ try std.testing.expectEqualDeep(expected, actual);
}
- };
-
- pub fn init(dest: Dest, payload: []const u8) Relay {
- return .{ .dest = dest, .payload = payload };
}
- /// Asserts that buf is large enough to fit the relay message.
- pub fn toBytes(self: Relay, buf: []u8) []u8 {
- var out: Writer = .fixed(buf);
- out.writeInt(u16, @intFromEnum(Message.relay), .big) catch unreachable;
- out.writeInt(u16, @intCast(self.payload.len + 4), .big) catch unreachable; // Length field, but unread. Will switch to checksum
- out.writeAll(&self.dest.bytes) catch unreachable;
- out.writeAll(self.payload) catch unreachable;
- return out.buffered();
- }
-
- test toBytes {
- var buf: [1024]u8 = undefined;
- const relay: Relay = .init(
- .fromBytes(&.{ 172, 18, 1, 30 }),
- // zig fmt: off
- &[_]u8{
- 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65,
- 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64
- },
- // zig fmt: on
- );
- // zig fmt: off
- var expected = [_]u8{
- 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72,
- 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65,
- 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64
- };
- // zig fmt: on
- try expectEqualMessageBuffers(&expected, relay.toBytes(&buf));
- }
-};
-
-const Connection = struct {
- src: u16,
- dest: u16,
- seq: u32,
- id: u32,
- reserved: u8 = undefined,
- options: Options = .{},
- payload: []const u8,
-
- /// Option values.
- /// Currently used!
- pub const Options = packed struct(u8) {
- opt1: bool = false,
- opt2: bool = false,
- opt3: bool = false,
- opt4: bool = false,
- opt5: bool = false,
- opt6: bool = false,
- opt7: bool = false,
- management: bool = false,
- };
-
- /// Asserts that buf is large enough to fit the connection message.
- pub fn toBytes(self: Connection, buf: []u8) []u8 {
- var out: Writer = .fixed(buf);
- out.writeInt(u16, @intFromEnum(Message.connection), .big) catch unreachable;
- out.writeInt(u16, @intCast(self.payload.len + 14), .big) catch unreachable; // Saprus length field, unread.
- out.writeInt(u16, self.src, .big) catch unreachable;
- out.writeInt(u16, self.dest, .big) catch unreachable;
- out.writeInt(u32, self.seq, .big) catch unreachable;
- out.writeInt(u32, self.id, .big) catch unreachable;
- out.writeByte(self.reserved) catch unreachable;
- out.writeStruct(self.options, .big) catch unreachable;
- out.writeAll(self.payload) catch unreachable;
- return out.buffered();
- }
-
- /// If the current message is a management message, return what kind.
- /// Else return null.
- pub fn management(self: Connection) MessageParseError!?Management {
- const b64_dec = std.base64.standard.Decoder;
- if (self.options.management) {
- var buf: [1]u8 = undefined;
- _ = b64_dec.decode(&buf, self.payload) catch return error.InvalidMessage;
-
- return switch (buf[0]) {
- 'P' => .ping,
- 'p' => .pong,
- else => error.UnknownSaprusType,
- };
+ test "Round trip" {
+ {
+ const expected = [_]u8{ 0x0, 0xe9, 0x0, 0x15, 0x30, 0x80, 0xf0, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x64, 0x36, 0x3a, 0x3a, 0x64, 0x61, 0x74, 0x61 };
+ const msg = (try parse(&expected)).connection;
+ var res_buf: [expected.len + 1]u8 = undefined; // + 1 to test subslice result.
+ const res = msg.toBytes(&res_buf);
+ try expectEqualMessageBuffers(&expected, res);
}
- return null;
}
- pub const Management = enum {
- ping,
- pong,
+ // Skip checking the length / checksum, because that is undefined.
+ fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void {
+ try std.testing.expectEqualSlices(u8, expected[0..2], actual[0..2]);
+ try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]);
+ }
+
+ pub const TypeError = error{
+ NotImplementedSaprusType,
+ UnknownSaprusType,
+ };
+ pub const ParseError = TypeError || error{
+ InvalidMessage,
};
};
-test "Round trip" {
- {
- const expected = [_]u8{ 0x0, 0xe9, 0x0, 0x15, 0x30, 0x80, 0xf0, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x64, 0x36, 0x3a, 0x3a, 0x64, 0x61, 0x74, 0x61 };
- const msg = (try parse(&expected)).connection;
- var res_buf: [expected.len + 1]u8 = undefined; // + 1 to test subslice result.
- const res = msg.toBytes(&res_buf);
- try expectEqualMessageBuffers(&expected, res);
- }
-}
-
-// Skip checking the length / checksum, because that is undefined.
-fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void {
- try std.testing.expectEqualSlices(u8, expected[0..2], actual[0..2]);
- try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]);
-}
+const relay_dest_len = 4;
const std = @import("std");
const Allocator = std.mem.Allocator;
diff --git a/src/root.zig b/src/root.zig
index aa78565..2a847fc 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -14,14 +14,16 @@
// You should have received a copy of the GNU General Public License along with
// Zaprus. If not, see .
+//! The Zaprus library is useful for implementing clients that interact with the [Saprus Protocol](https://gitlab.com/c2-games/red-team/saprus).
+//!
+//! The main entrypoint into this library is the `Client` type.
+//! It can be used to send fire and forget messages, and establish persistent connections.
+//! It is up to the consumer of this library to handle non-management message payloads.
+//! The library handles management messages automatically (right now, just ping).
+
pub const Client = @import("Client.zig");
pub const Connection = @import("Connection.zig");
-
-const msg = @import("message.zig");
-
-pub const MessageTypeError = msg.MessageTypeError;
-pub const MessageParseError = msg.MessageParseError;
-pub const Message = msg.Message;
+pub const Message = @import("message.zig").Message;
test {
@import("std").testing.refAllDecls(@This());