Clean API and add docs

This commit is contained in:
2026-02-01 19:35:14 -05:00
parent 558f40213b
commit 2c9e648c2c
7 changed files with 248 additions and 215 deletions

View File

@@ -14,6 +14,8 @@
// 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;
@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -14,230 +14,219 @@
// 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 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;

View File

@@ -14,14 +14,16 @@
// You should have received a copy of the GNU General Public License along with
// Zaprus. If not, see <https://www.gnu.org/licenses/>.
//! 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());