6 Commits

Author SHA1 Message Date
f202410f0d Include license and readme in build output
This is probably required for GPL compliance :-D
2026-02-03 23:06:30 -05:00
9dcbc44aa1 ReleaseSmall by default 2026-02-03 22:50:24 -05:00
d1fdf0c1be Automatically publish binaries to nextcloud
Updated the releases directory to use a specific account for releases
2026-02-03 22:44:53 -05:00
f13221133d Set dev version to 0.0.0 2026-02-01 21:10:50 -05:00
2c9e648c2c Clean API and add docs 2026-02-01 21:02:38 -05:00
558f40213b Update to Saprus 0.2.1
Handle management messages instead of letting them bubble up through the
connection to the consumer.
Right now, this just means handling ping messages by sending a pong.

Also updated to follow the new handshake flow.
The sentinel will mirror the ports instead of matching them.

Now filters on the full source and dest ports, which are less likely to
have erroneous matches.
2026-02-01 19:16:22 -05:00
11 changed files with 368 additions and 228 deletions

View File

@@ -0,0 +1,26 @@
when:
- event: ["push", "pull_request", "manual"]
branch: ["dev", "master", "test*"]
tag: ["v*"]
engine: "nixery"
dependencies:
nixpkgs:
- rclone
git+https://github.com/mitchellh/zig-overlay:
- master
steps:
- name: "Build"
command: "zig build -Doptimize=ReleaseSmall -Dcpu=baseline"
- name: "Publish"
command: |
rclone sync ./zig-out \
--webdav-url "$RELEASE_NEXTCLOUD_HOST/remote.php/dav/files/$RELEASE_NEXTCLOUD_USER/" \
--webdav-user "$RELEASE_NEXTCLOUD_USER" \
--webdav-pass "$RELEASE_NEXTCLOUD_PASS" \
--webdav-vendor nextcloud \
:webdav:"zaprus/zaprus-$TANGLED_REF_NAME" \
-q

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# 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/7jJPTm68Zp3mN8F).
The code for this can be found here:
- https://tangled.org/zambyte.robbyzambito.me/zaprus
- https://git.robbyzambito.me/zaprus

View File

@@ -7,6 +7,9 @@ const std = @import("std");
// build runner to parallelize the build automatically (and the cache system to // build runner to parallelize the build automatically (and the cache system to
// know when a step doesn't need to be re-run). // know when a step doesn't need to be re-run).
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
// Ensure the license is included in the output directory
b.installFile("LICENSE.md", "LICENSE.md");
b.installFile("README.md", "README.md");
// Standard target options allow the person running `zig build` to choose // Standard target options allow the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which // what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options // means any target is allowed, and the default is native. Other options
@@ -21,6 +24,9 @@ pub fn build(b: *std.Build) void {
// target and optimize options) will be listed when running `zig build --help` // target and optimize options) will be listed when running `zig build --help`
// in this directory. // 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 // This creates a module, which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries. // some compilation options, such as optimization mode and linked system libraries.
// Zig modules are the preferred way of making Zig code available to consumers. // Zig modules are the preferred way of making Zig code available to consumers.
@@ -41,6 +47,22 @@ pub fn build(b: *std.Build) void {
.target = target, .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 // Create static library
const lib = b.addLibrary(.{ const lib = b.addLibrary(.{
.name = "zaprus", .name = "zaprus",

View File

@@ -10,7 +10,7 @@
// This is a [Semantic Version](https://semver.org/). // This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication. // In a future version of Zig it will be used for package deduplication.
.version = "0.1.0", .version = "0.0.0",
// Together with name, this represents a globally unique package // Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the // identifier. This field is generated by the Zig toolchain when the

View File

@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License along with // You should have received a copy of the GNU General Public License along with
// Zaprus. If not, see <https://www.gnu.org/licenses/>. // 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_enc = std.base64.standard.Encoder;
const base64_dec = std.base64.standard.Decoder; const base64_dec = std.base64.standard.Decoder;
@@ -37,6 +39,8 @@ pub fn deinit(self: *Client) void {
self.* = undefined; 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 { pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void {
const io_source: std.Random.IoSource = .{ .io = io }; const io_source: std.Random.IoSource = .{ .io = io };
const rand = io_source.interface(); 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); 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 io_source: std.Random.IoSource = .{ .io = io };
const rand = io_source.interface(); const rand = io_source.interface();
@@ -100,7 +105,7 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
var connection: SaprusMessage = .{ var connection: SaprusMessage = .{
.connection = .{ .connection = .{
.src = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)), .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, .seq = undefined,
.id = undefined, .id = undefined,
.payload = payload, .payload = payload,
@@ -108,7 +113,7 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
}; };
log.debug("Setting bpf filter to port {}", .{connection.connection.src}); 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}); log.err("Failed to set port filter: {t}", .{err});
return err; return err;
}; };
@@ -131,7 +136,17 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
log.debug("Awaiting handshake response", .{}); log.debug("Awaiting handshake response", .{});
// Ignore response from sentinel, just accept that we got one. // 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.udp.dst_port = udp_dest_port;
headers.ip.id = rand.int(u16); headers.ip.id = rand.int(u16);
@@ -147,12 +162,17 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection {
try self.socket.send(full_msg); 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 RawSocket = @import("./RawSocket.zig");
const SaprusMessage = @import("message.zig").Message; const SaprusMessage = @import("message.zig").Message;
const saprusParse = SaprusMessage.parse;
const SaprusConnection = @import("Connection.zig"); const SaprusConnection = @import("Connection.zig");
const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp; const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp;

View File

@@ -20,33 +20,61 @@ connection: SaprusMessage,
const Connection = @This(); const Connection = @This();
pub fn init(socket: RawSocket, headers: EthIpUdp, connection: SaprusMessage) Connection { // 'p' as base64
return .{ const pong = "cA==";
.socket = socket,
.headers = headers,
.connection = connection,
};
}
pub fn next(self: Connection, io: Io, buf: []u8) ![]const u8 { /// Attempts to read from the network, and returns the next message, if any.
_ = io; ///
/// 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", .{}); log.debug("Awaiting connection message", .{});
const res = try self.socket.receive(buf); const res = try self.socket.receive(buf);
log.debug("Received {} byte connection message", .{res.len}); log.debug("Received {} byte connection message", .{res.len});
const msg: SaprusMessage = try .parse(res[42..]); const msg = SaprusMessage.parse(res[42..]) catch |err| {
const connection_res = msg.connection; log.err("Failed to parse next message: {t}\n{x}\n{x}", .{ err, res[0..], res[42..] });
return err;
};
log.debug("Payload was {s}", .{connection_res.payload}); switch (msg) {
.connection => |con_res| {
return connection_res.payload; 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 io_source: std.Random.IoSource = .{ .io = io };
const rand = io_source.interface(); const rand = io_source.interface();
log.debug("Sending connection message", .{}); log.debug("Sending connection message", .{});
self.connection.connection.options = options;
self.connection.connection.payload = buf; self.connection.connection.payload = buf;
var connection_bytes_buf: [2048]u8 = undefined; var connection_bytes_buf: [2048]u8 = undefined;
const connection_bytes = self.connection.toBytes(&connection_bytes_buf); const connection_bytes = self.connection.toBytes(&connection_bytes_buf);

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; 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; if (socket < 0) return error.SocketError;
@@ -133,7 +138,7 @@ pub fn receive(self: RawSocket, buf: []u8) ![]u8 {
return buf[0..len]; 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; const BPF = std.os.linux.BPF;
// BPF instruction structure for classic BPF // BPF instruction structure for classic BPF
const SockFilter = extern struct { const SockFilter = extern struct {
@@ -149,11 +154,26 @@ pub fn attachSaprusPortFilter(self: RawSocket, port: u16) !void {
}; };
// Build the filter program // Build the filter program
const filter = [_]SockFilter{ const filter = if (incoming_src_port) |inc_src| &[_]SockFilter{
// Load 2 bytes at offset 46 (absolute) // Load 2 bytes at offset 46 (absolute)
.{ .code = BPF.LD | BPF.H | BPF.ABS, .jt = 0, .jf = 0, .k = 46 }, .{ .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) // 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)
.{ .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, incoming_dest_port) },
// Return 0xffff (pass) // Return 0xffff (pass)
.{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0xffff }, .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0xffff },
// Return 0x0 (fail) // Return 0x0 (fail)
@@ -161,8 +181,8 @@ pub fn attachSaprusPortFilter(self: RawSocket, port: u16) !void {
}; };
const fprog = SockFprog{ const fprog = SockFprog{
.len = filter.len, .len = @intCast(filter.len),
.filter = &filter, .filter = filter.ptr,
}; };
// Attach filter to socket using setsockopt // Attach filter to socket using setsockopt

View File

@@ -99,6 +99,6 @@ export fn zaprus_connection_send(
const c: ?*zaprus.Connection = @ptrCast(@alignCast(connection)); const c: ?*zaprus.Connection = @ptrCast(@alignCast(connection));
const zc = c orelse return 1; 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; return 0;
} }

View File

@@ -191,6 +191,7 @@ pub fn main(init: std.process.Init) !void {
error.SymLinkLoop, error.SymLinkLoop,
error.SystemResources, error.SystemResources,
=> blk: { => blk: {
log.debug("Trying to execute command directly: {s}", .{connection_payload});
var argv_buf: [128][]const u8 = undefined; var argv_buf: [128][]const u8 = undefined;
var argv: ArrayList([]const u8) = .initBuffer(&argv_buf); var argv: ArrayList([]const u8) = .initBuffer(&argv_buf);
var payload_iter = std.mem.splitAny(u8, connection_payload, " \t\n"); var payload_iter = std.mem.splitAny(u8, connection_payload, " \t\n");
@@ -229,7 +230,7 @@ pub fn main(init: std.process.Init) !void {
error.EndOfStream => { error.EndOfStream => {
cmd_output.print("{b64}", .{child_output_reader.interface.buffered()}) catch unreachable; cmd_output.print("{b64}", .{child_output_reader.interface.buffered()}) catch unreachable;
if (cmd_output.end > 0) { 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}); log.debug("Failed to send connection chunk: {t}", .{e});
continue :next_message; continue :next_message;
}; };
@@ -238,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; 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}); log.debug("Failed to send connection chunk: {t}", .{err});
continue :next_message; continue :next_message;
}; };

View File

@@ -14,37 +14,128 @@
// You should have received a copy of the GNU General Public License along with // You should have received a copy of the GNU General Public License along with
// Zaprus. If not, see <https://www.gnu.org/licenses/>. // 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)) { pub const Message = union(enum(u16)) {
relay: Message.Relay = 0x003C, relay: Message.Relay = 0x003C,
connection: Message.Connection = 0x00E9, connection: Message.Connection = 0x00E9,
_, _,
pub const Relay = message.Relay; pub const Relay = struct {
pub const Connection = message.Connection; 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) { return switch (self) {
inline .relay, .connection => |m| m.toBytes(buf), inline .relay, .connection => |m| m.toBytes(buf),
else => unreachable, else => unreachable,
}; };
} }
pub const parse = message.parse; pub fn parse(bytes: []const u8) ParseError!Message {
};
pub const relay_dest_len = 4;
pub fn parse(bytes: []const u8) MessageParseError!Message {
var in: Reader = .fixed(bytes); var in: Reader = .fixed(bytes);
const @"type" = in.takeEnum(std.meta.Tag(Message), .big) catch |err| switch (err) { const @"type" = in.takeEnum(std.meta.Tag(Message), .big) catch |err| switch (err) {
error.InvalidEnumTag => return error.UnknownSaprusType, error.InvalidEnumTag => return error.UnknownSaprusType,
@@ -110,97 +201,6 @@ test parse {
} }
} }
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 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 = 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,
};
/// 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" { 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 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 };
@@ -217,11 +217,22 @@ fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void {
try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]); try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]);
} }
pub const TypeError = error{
NotImplementedSaprusType,
UnknownSaprusType,
};
pub const ParseError = TypeError || error{
InvalidMessage,
};
};
const relay_dest_len = 4;
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer; const Writer = std.Io.Writer;
const Reader = std.Io.Reader; const Reader = std.Io.Reader;
test { test {
std.testing.refAllDeclsRecursive(@This()); std.testing.refAllDecls(@This());
} }

View File

@@ -14,15 +14,16 @@
// You should have received a copy of the GNU General Public License along with // You should have received a copy of the GNU General Public License along with
// Zaprus. If not, see <https://www.gnu.org/licenses/>. // 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 Client = @import("Client.zig");
pub const Connection = @import("Connection.zig"); pub const Connection = @import("Connection.zig");
pub const Message = @import("message.zig").Message;
const msg = @import("message.zig");
pub const PacketType = msg.PacketType;
pub const MessageTypeError = msg.MessageTypeError;
pub const MessageParseError = msg.MessageParseError;
pub const Message = msg.Message;
test { test {
@import("std").testing.refAllDecls(@This()); @import("std").testing.refAllDecls(@This());