mirror of
https://git.robbyzambito.me/zaprus
synced 2026-02-04 08:24:52 +00:00
Compare commits
3 Commits
daf18d3552
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d1ca448835 | |||
| 2c9e648c2c | |||
| 558f40213b |
6
README.md
Normal file
6
README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 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/cNaLeDz38W5ZcZs).
|
||||||
19
build.zig
19
build.zig
@@ -21,6 +21,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 +44,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",
|
||||||
|
|||||||
@@ -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.2.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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
247
src/message.zig
247
src/message.zig
@@ -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,
|
||||||
@@ -87,9 +178,9 @@ pub fn parse(bytes: []const u8) MessageParseError!Message {
|
|||||||
},
|
},
|
||||||
else => return error.NotImplementedSaprusType,
|
else => return error.NotImplementedSaprusType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test parse {
|
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 });
|
_ = 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 });
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -108,100 +199,9 @@ test parse {
|
|||||||
|
|
||||||
try std.testing.expectEqualDeep(expected, actual);
|
try std.testing.expectEqualDeep(expected, actual);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const Relay = struct {
|
|
||||||
dest: Dest,
|
|
||||||
checksum: [2]u8 = undefined,
|
|
||||||
payload: []const u8,
|
|
||||||
|
|
||||||
pub const Dest = struct {
|
|
||||||
bytes: [relay_dest_len]u8,
|
|
||||||
|
|
||||||
/// Asserts bytes is less than or equal to 4 bytes
|
|
||||||
pub fn fromBytes(bytes: []const u8) Dest {
|
|
||||||
var buf: [4]u8 = @splat(0);
|
|
||||||
std.debug.assert(bytes.len <= buf.len);
|
|
||||||
@memcpy(buf[0..bytes.len], bytes);
|
|
||||||
return .{ .bytes = buf };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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.
|
test "Round trip" {
|
||||||
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" {
|
|
||||||
{
|
{
|
||||||
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 };
|
||||||
const msg = (try parse(&expected)).connection;
|
const msg = (try parse(&expected)).connection;
|
||||||
@@ -209,13 +209,24 @@ test "Round trip" {
|
|||||||
const res = msg.toBytes(&res_buf);
|
const res = msg.toBytes(&res_buf);
|
||||||
try expectEqualMessageBuffers(&expected, res);
|
try expectEqualMessageBuffers(&expected, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip checking the length / checksum, because that is undefined.
|
// Skip checking the length / checksum, because that is undefined.
|
||||||
fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void {
|
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[0..2], actual[0..2]);
|
||||||
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;
|
||||||
@@ -223,5 +234,5 @@ 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());
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/root.zig
15
src/root.zig
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user