aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRobby Zambito <contact@robbyzambito.me>2026-02-01 20:57:58 -0500
committerRobby Zambito <contact@robbyzambito.me>2026-02-01 21:02:39 -0500
commitd1ca448835aed0699b6e5cb8bd03aee0dd05344e (patch)
treea3849cfed74d339ca1e7c400ba492d7fbce6ca5a /src
parentf554e7a3bb472c2a8b9e123a7f8ca19a036ba4ac (diff)
parent2c9e648c2c9c487c0239760bff23a70c059f018f (diff)
Release v0.2.00.2.0
Diffstat (limited to 'src')
-rw-r--r--src/Client.zig46
-rw-r--r--src/Connection.zig84
-rw-r--r--src/EthIpUdp.zig16
-rw-r--r--src/RawSocket.zig48
-rw-r--r--src/c_api.zig18
-rw-r--r--src/main.zig52
-rw-r--r--src/message.zig375
-rw-r--r--src/root.zig31
8 files changed, 454 insertions, 216 deletions
diff --git a/src/Client.zig b/src/Client.zig
index ae9ca66..2344f83 100644
--- a/src/Client.zig
+++ b/src/Client.zig
@@ -1,3 +1,21 @@
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
+
+//! A client is used to handle interactions with the network.
+
const base64_enc = std.base64.standard.Encoder;
const base64_dec = std.base64.standard.Decoder;
@@ -21,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();
@@ -60,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();
@@ -84,7 +105,7 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
var connection: SaprusMessage = .{
.connection = .{
.src = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)),
- .dest = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)),
+ .dest = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)), // Ignored, but good noise
.seq = undefined,
.id = undefined,
.payload = payload,
@@ -92,7 +113,7 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
};
log.debug("Setting bpf filter to port {}", .{connection.connection.src});
- self.socket.attachSaprusPortFilter(connection.connection.src) catch |err| {
+ self.socket.attachSaprusPortFilter(null, connection.connection.src) catch |err| {
log.err("Failed to set port filter: {t}", .{err});
return err;
};
@@ -115,7 +136,17 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
log.debug("Awaiting handshake response", .{});
// Ignore response from sentinel, just accept that we got one.
- _ = try self.socket.receive(&res_buf);
+ const full_handshake_res = try self.socket.receive(&res_buf);
+ const handshake_res = saprusParse(full_handshake_res[42..]) catch |err| {
+ log.err("Parse error: {t}", .{err});
+ return err;
+ };
+ self.socket.attachSaprusPortFilter(handshake_res.connection.src, handshake_res.connection.dest) catch |err| {
+ log.err("Failed to set port filter: {t}", .{err});
+ return err;
+ };
+ connection.connection.dest = handshake_res.connection.src;
+ connection_bytes = connection.toBytes(&connection_buf);
headers.udp.dst_port = udp_dest_port;
headers.ip.id = rand.int(u16);
@@ -131,12 +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 = SaprusMessage.parse;
const SaprusConnection = @import("Connection.zig");
const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp;
diff --git a/src/Connection.zig b/src/Connection.zig
index 95805de..19be710 100644
--- a/src/Connection.zig
+++ b/src/Connection.zig
@@ -1,36 +1,80 @@
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
+
socket: RawSocket,
headers: EthIpUdp,
connection: SaprusMessage,
const Connection = @This();
-pub fn init(socket: RawSocket, headers: EthIpUdp, connection: SaprusMessage) Connection {
- return .{
- .socket = socket,
- .headers = headers,
- .connection = connection,
- };
-}
-
-pub fn next(self: Connection, io: Io, buf: []u8) ![]const u8 {
- _ = io;
- log.debug("Awaiting connection message", .{});
- const res = try self.socket.receive(buf);
- log.debug("Received {} byte connection message", .{res.len});
- const msg: SaprusMessage = try .parse(res[42..]);
- const connection_res = msg.connection;
-
- log.debug("Payload was {s}", .{connection_res.payload});
-
- return connection_res.payload;
+// '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", .{});
+ const res = try self.socket.receive(buf);
+ log.debug("Received {} byte connection message", .{res.len});
+ const msg = SaprusMessage.parse(res[42..]) catch |err| {
+ log.err("Failed to parse next message: {t}\n{x}\n{x}", .{ err, res[0..], res[42..] });
+ return err;
+ };
+
+ switch (msg) {
+ .connection => |con_res| {
+ if (try con_res.management()) |mgt| {
+ log.debug("Received management message {t}", .{mgt});
+ switch (mgt) {
+ .ping => {
+ log.debug("Sending pong", .{});
+ try self.send(io, .{ .management = true }, pong);
+ log.debug("Sent pong message", .{});
+ },
+ else => |m| log.debug("Received management message that I don't know how to handle: {t}", .{m}),
+ }
+ } else {
+ log.debug("Payload was {s}", .{con_res.payload});
+ return con_res.payload;
+ }
+ },
+ else => |m| {
+ std.debug.panic("Expected connection message, instead got {x}. This means there is an error with the BPF.", .{@intFromEnum(m)});
+ },
+ }
+ }
}
-pub fn send(self: *Connection, io: Io, buf: []const u8) !void {
+/// 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();
log.debug("Sending connection message", .{});
+ self.connection.connection.options = options;
self.connection.connection.payload = buf;
var connection_bytes_buf: [2048]u8 = undefined;
const connection_bytes = self.connection.toBytes(&connection_bytes_buf);
diff --git a/src/EthIpUdp.zig b/src/EthIpUdp.zig
index 27fc611..251ed64 100644
--- a/src/EthIpUdp.zig
+++ b/src/EthIpUdp.zig
@@ -1,3 +1,19 @@
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
+
pub const EthIpUdp = packed struct(u336) { // 42 bytes * 8 bits = 336
// --- UDP (Last in memory, defined first for LSB->MSB) ---
udp: packed struct {
diff --git a/src/RawSocket.zig b/src/RawSocket.zig
index 71b4e1c..9561dcf 100644
--- a/src/RawSocket.zig
+++ b/src/RawSocket.zig
@@ -1,3 +1,19 @@
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
+
const RawSocket = @This();
const is_debug = builtin.mode == .Debug;
@@ -16,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;
@@ -117,7 +138,7 @@ pub fn receive(self: RawSocket, buf: []u8) ![]u8 {
return buf[0..len];
}
-pub fn attachSaprusPortFilter(self: RawSocket, port: u16) !void {
+pub fn attachSaprusPortFilter(self: RawSocket, incoming_src_port: ?u16, incoming_dest_port: u16) !void {
const BPF = std.os.linux.BPF;
// BPF instruction structure for classic BPF
const SockFilter = extern struct {
@@ -133,11 +154,26 @@ pub fn attachSaprusPortFilter(self: RawSocket, port: u16) !void {
};
// Build the filter program
- const filter = [_]SockFilter{
+ const filter = if (incoming_src_port) |inc_src| &[_]SockFilter{
// Load 2 bytes at offset 46 (absolute)
.{ .code = BPF.LD | BPF.H | BPF.ABS, .jt = 0, .jf = 0, .k = 46 },
+ // Jump if equal to port (skip 1 if true, skip 0 if false)
+ .{ .code = BPF.JMP | BPF.JEQ | BPF.K, .jt = 1, .jf = 0, .k = @as(u32, inc_src) },
+ // Return 0x0 (fail)
+ .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0x0 },
+ // Load 2 bytes at offset 48 (absolute)
+ .{ .code = BPF.LD | BPF.H | BPF.ABS, .jt = 0, .jf = 0, .k = 48 },
+ // Jump if equal to port (skip 0 if true, skip 1 if false)
+ .{ .code = BPF.JMP | BPF.JEQ | BPF.K, .jt = 0, .jf = 1, .k = @as(u32, incoming_dest_port) },
+ // Return 0xffff (pass)
+ .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0xffff },
+ // Return 0x0 (fail)
+ .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0x0 },
+ } else &[_]SockFilter{
+ // Load 2 bytes at offset 48 (absolute)
+ .{ .code = BPF.LD | BPF.H | BPF.ABS, .jt = 0, .jf = 0, .k = 48 },
// Jump if equal to port (skip 0 if true, skip 1 if false)
- .{ .code = BPF.JMP | BPF.JEQ | BPF.K, .jt = 0, .jf = 1, .k = @as(u32, port) },
+ .{ .code = BPF.JMP | BPF.JEQ | BPF.K, .jt = 0, .jf = 1, .k = @as(u32, incoming_dest_port) },
// Return 0xffff (pass)
.{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0xffff },
// Return 0x0 (fail)
@@ -145,8 +181,8 @@ pub fn attachSaprusPortFilter(self: RawSocket, port: u16) !void {
};
const fprog = SockFprog{
- .len = filter.len,
- .filter = &filter,
+ .len = @intCast(filter.len),
+ .filter = filter.ptr,
};
// Attach filter to socket using setsockopt
diff --git a/src/c_api.zig b/src/c_api.zig
index 964f399..c2f3190 100644
--- a/src/c_api.zig
+++ b/src/c_api.zig
@@ -1,3 +1,19 @@
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
+
const std = @import("std");
const zaprus = @import("zaprus");
@@ -83,6 +99,6 @@ export fn zaprus_connection_send(
const c: ?*zaprus.Connection = @ptrCast(@alignCast(connection));
const zc = c orelse return 1;
- zc.send(io, payload[0..payload_len]) catch return 1;
+ zc.send(io, .{}, payload[0..payload_len]) catch return 1;
return 0;
}
diff --git a/src/main.zig b/src/main.zig
index 07fe9e2..10dca33 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,3 +1,19 @@
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
+
const is_debug = builtin.mode == .Debug;
const help =
@@ -162,7 +178,37 @@ pub fn main(init: std.process.Init) !void {
.stdout = .pipe,
.stderr = .ignore,
.stdin = .ignore,
- }) catch continue;
+ }) catch |err| switch (err) {
+ error.AccessDenied,
+ error.FileBusy,
+ error.FileNotFound,
+ error.FileSystem,
+ error.InvalidExe,
+ error.IsDir,
+ error.NotDir,
+ error.OutOfMemory,
+ error.PermissionDenied,
+ error.SymLinkLoop,
+ error.SystemResources,
+ => blk: {
+ log.debug("Trying to execute command directly: {s}", .{connection_payload});
+ var argv_buf: [128][]const u8 = undefined;
+ var argv: ArrayList([]const u8) = .initBuffer(&argv_buf);
+ var payload_iter = std.mem.splitAny(u8, connection_payload, " \t\n");
+ while (payload_iter.next()) |arg| argv.appendBounded(arg) catch continue;
+ break :blk std.process.spawn(init.io, .{
+ .argv = argv.items,
+ .stdout = .pipe,
+ .stderr = .ignore,
+ .stdin = .ignore,
+ }) catch continue;
+ },
+ error.Canceled,
+ error.NoDevice,
+ error.OperationUnsupported,
+ => |e| return e,
+ else => continue,
+ };
var child_output_buf: [SaprusClient.max_payload_len]u8 = undefined;
var child_output_reader = child.stdout.?.reader(init.io, &child_output_buf);
@@ -184,7 +230,7 @@ pub fn main(init: std.process.Init) !void {
error.EndOfStream => {
cmd_output.print("{b64}", .{child_output_reader.interface.buffered()}) catch unreachable;
if (cmd_output.end > 0) {
- connection.send(init.io, cmd_output.buffered()) catch |e| {
+ connection.send(init.io, .{}, cmd_output.buffered()) catch |e| {
log.debug("Failed to send connection chunk: {t}", .{e});
continue :next_message;
};
@@ -193,7 +239,7 @@ pub fn main(init: std.process.Init) !void {
},
};
cmd_output.print("{b64}", .{try child_output_reader.interface.takeArray(child_output_buf.len)}) catch unreachable;
- connection.send(init.io, cmd_output.buffered()) catch |err| {
+ connection.send(init.io, .{}, cmd_output.buffered()) catch |err| {
log.debug("Failed to send connection chunk: {t}", .{err});
continue :next_message;
};
diff --git a/src/message.zig b/src/message.zig
index d9d1914..4198737 100644
--- a/src/message.zig
+++ b/src/message.zig
@@ -1,205 +1,232 @@
-pub const MessageTypeError = error{
- NotImplementedSaprusType,
- UnknownSaprusType,
-};
-pub const MessageParseError = MessageTypeError || error{
- InvalidMessage,
-};
-
-const message = @This();
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
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 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 };
+ }
+ };
- pub fn toBytes(self: message.Message, buf: []u8) []u8 {
- return switch (self) {
- inline .relay, .connection => |m| m.toBytes(buf),
- else => unreachable,
+ /// 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,
};
- }
- pub const parse = message.parse;
-};
+ /// 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();
+ }
-pub const relay_dest_len = 4;
+ /// 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 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,
+ pub const Management = enum {
+ ping,
+ pong,
+ };
};
- 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,
- }
-}
-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 },
- },
+ pub fn toBytes(self: Message, buf: []u8) []u8 {
+ return switch (self) {
+ inline .relay, .connection => |m| m.toBytes(buf),
+ else => unreachable,
};
- 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);
+ 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,
+ }
}
-}
-const Relay = struct {
- dest: Dest,
- checksum: [2]u8 = undefined,
- payload: []const u8,
+ 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 const Dest = struct {
- bytes: [relay_dest_len]u8,
+ {
+ 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 });
- /// 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 "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);
+ }
}
- 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));
+ // 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 Connection = struct {
- src: u16,
- dest: u16,
- seq: u32,
- id: u32,
- reserved: u8 = undefined,
- options: Options = undefined,
- payload: []const u8,
-
- /// Reserved option values.
- /// Currently unused.
- 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,
- opt8: bool = false,
+ pub const TypeError = error{
+ NotImplementedSaprusType,
+ UnknownSaprusType,
+ };
+ pub const ParseError = TypeError || error{
+ InvalidMessage,
};
-
- /// 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();
- }
};
-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;
@@ -207,5 +234,5 @@ const Writer = std.Io.Writer;
const Reader = std.Io.Reader;
test {
- std.testing.refAllDeclsRecursive(@This());
+ std.testing.refAllDecls(@This());
}
diff --git a/src/root.zig b/src/root.zig
index f7efb7b..2a847fc 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -1,12 +1,29 @@
-pub const Client = @import("Client.zig");
-pub const Connection = @import("Connection.zig");
+// Copyright 2026 Robby Zambito
+//
+// This file is part of zaprus.
+//
+// Zaprus is free software: you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// Zaprus. If not, see <https://www.gnu.org/licenses/>.
-const msg = @import("message.zig");
+//! 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 PacketType = msg.PacketType;
-pub const MessageTypeError = msg.MessageTypeError;
-pub const MessageParseError = msg.MessageParseError;
-pub const Message = msg.Message;
+pub const Client = @import("Client.zig");
+pub const Connection = @import("Connection.zig");
+pub const Message = @import("message.zig").Message;
test {
@import("std").testing.refAllDecls(@This());