Adversarial C2 Protocol Implemented in Zig

Clean API and add docs

+241 -208
+6
README.md
··· 1 + # zaprus 2 + 3 + This is an implementation of the [Saprus protocol](https://gitlab.com/c2-games/red-team/saprus) in Zig. 4 + It is useful for developing clients either in Zig, or in any other language using the C bindings. 5 + 6 + Binary releases can be downloaded [here](https://cloud.zambito.xyz/s/cNaLeDz38W5ZcZs).
+19
build.zig
··· 21 21 // target and optimize options) will be listed when running `zig build --help` 22 22 // in this directory. 23 23 24 + // Get default install step (called with `zig build` or `zig build install`) 25 + const install_step = b.getInstallStep(); 26 + 24 27 // This creates a module, which represents a collection of source files alongside 25 28 // some compilation options, such as optimization mode and linked system libraries. 26 29 // Zig modules are the preferred way of making Zig code available to consumers. ··· 40 43 // which requires us to specify a target. 41 44 .target = target, 42 45 }); 46 + 47 + // Only used to generate the documentation 48 + const zaprus_lib = b.addLibrary(.{ 49 + .name = "zaprus", 50 + .root_module = mod, 51 + }); 52 + 53 + const docs_step = b.step("doc", "Emit documentation"); 54 + const docs_install = b.addInstallDirectory(.{ 55 + .install_dir = .prefix, 56 + .install_subdir = "docs", 57 + .source_dir = zaprus_lib.getEmittedDocs(), 58 + }); 59 + 60 + docs_step.dependOn(&docs_install.step); 61 + install_step.dependOn(docs_step); 43 62 44 63 // Create static library 45 64 const lib = b.addLibrary(.{
+12 -3
src/Client.zig
··· 14 14 // You should have received a copy of the GNU General Public License along with 15 15 // Zaprus. If not, see <https://www.gnu.org/licenses/>. 16 16 17 + //! A client is used to handle interactions with the network. 18 + 17 19 const base64_enc = std.base64.standard.Encoder; 18 20 const base64_dec = std.base64.standard.Decoder; 19 21 ··· 37 39 self.* = undefined; 38 40 } 39 41 42 + /// Sends a fire and forget message over the network. 43 + /// This function asserts that `payload` fits within a single packet. 40 44 pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void { 41 45 const io_source: std.Random.IoSource = .{ .io = io }; 42 46 const rand = io_source.interface(); ··· 76 80 try self.socket.send(full_msg); 77 81 } 78 82 79 - pub fn connect(self: Client, io: Io, payload: []const u8) !SaprusConnection { 83 + /// Attempts to establish a new connection with the sentinel. 84 + pub fn connect(self: Client, io: Io, payload: []const u8) (error{ BpfAttachFailed, Timeout } || SaprusMessage.ParseError)!SaprusConnection { 80 85 const io_source: std.Random.IoSource = .{ .io = io }; 81 86 const rand = io_source.interface(); 82 87 ··· 157 162 158 163 try self.socket.send(full_msg); 159 164 160 - return .init(self.socket, headers, connection); 165 + return .{ 166 + .socket = self.socket, 167 + .headers = headers, 168 + .connection = connection, 169 + }; 161 170 } 162 171 163 172 const RawSocket = @import("./RawSocket.zig"); 164 173 165 174 const SaprusMessage = @import("message.zig").Message; 166 - const saprusParse = @import("message.zig").parse; 175 + const saprusParse = SaprusMessage.parse; 167 176 const SaprusConnection = @import("Connection.zig"); 168 177 const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp; 169 178
+11 -8
src/Connection.zig
··· 20 20 21 21 const Connection = @This(); 22 22 23 - pub fn init(socket: RawSocket, headers: EthIpUdp, connection: SaprusMessage) Connection { 24 - return .{ 25 - .socket = socket, 26 - .headers = headers, 27 - .connection = connection, 28 - }; 29 - } 30 - 31 23 // 'p' as base64 32 24 const pong = "cA=="; 33 25 26 + /// Attempts to read from the network, and returns the next message, if any. 27 + /// 28 + /// Asserts that `buf` is large enough to store the message that is received. 29 + /// 30 + /// This will internally process management messages, and return the message 31 + /// payload for the next non management connection message. 32 + /// This function is ignorant to the message encoding. 34 33 pub fn next(self: *Connection, io: Io, buf: []u8) ![]const u8 { 35 34 while (true) { 36 35 log.debug("Awaiting connection message", .{}); ··· 65 64 } 66 65 } 67 66 67 + /// Attempts to write a message to the network. 68 + /// 69 + /// Clients should pass `.{}` for options unless you know what you are doing. 70 + /// `buf` will be sent over the network as-is; this function is ignorant of encoding. 68 71 pub fn send(self: *Connection, io: Io, options: SaprusMessage.Connection.Options, buf: []const u8) !void { 69 72 const io_source: std.Random.IoSource = .{ .io = io }; 70 73 const rand = io_source.interface();
+6 -1
src/RawSocket.zig
··· 32 32 }, 33 33 }; 34 34 35 - pub fn init() !RawSocket { 35 + pub fn init() error{ 36 + SocketError, 37 + NicError, 38 + NoInterfaceFound, 39 + BindError, 40 + }!RawSocket { 36 41 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; 37 42 if (socket < 0) return error.SocketError; 38 43
+179 -190
src/message.zig
··· 14 14 // You should have received a copy of the GNU General Public License along with 15 15 // Zaprus. If not, see <https://www.gnu.org/licenses/>. 16 16 17 - pub const MessageTypeError = error{ 18 - NotImplementedSaprusType, 19 - UnknownSaprusType, 20 - }; 21 - pub const MessageParseError = MessageTypeError || error{ 22 - InvalidMessage, 23 - }; 24 - 25 - const message = @This(); 26 - 27 17 pub const Message = union(enum(u16)) { 28 18 relay: Message.Relay = 0x003C, 29 19 connection: Message.Connection = 0x00E9, 30 20 _, 31 21 32 - pub const Relay = message.Relay; 33 - pub const Connection = message.Connection; 22 + pub const Relay = struct { 23 + dest: Dest, 24 + checksum: [2]u8 = undefined, 25 + payload: []const u8, 34 26 35 - pub fn toBytes(self: message.Message, buf: []u8) []u8 { 36 - return switch (self) { 37 - inline .relay, .connection => |m| m.toBytes(buf), 38 - else => unreachable, 27 + pub const Dest = struct { 28 + bytes: [relay_dest_len]u8, 29 + 30 + /// Asserts bytes is less than or equal to 4 bytes 31 + pub fn fromBytes(bytes: []const u8) Dest { 32 + var buf: [4]u8 = @splat(0); 33 + std.debug.assert(bytes.len <= buf.len); 34 + @memcpy(buf[0..bytes.len], bytes); 35 + return .{ .bytes = buf }; 36 + } 39 37 }; 40 - } 41 38 42 - pub const parse = message.parse; 43 - }; 39 + /// Asserts that buf is large enough to fit the relay message. 40 + pub fn toBytes(self: Relay, buf: []u8) []u8 { 41 + var out: Writer = .fixed(buf); 42 + out.writeInt(u16, @intFromEnum(Message.relay), .big) catch unreachable; 43 + out.writeInt(u16, @intCast(self.payload.len + 4), .big) catch unreachable; // Length field, but unread. Will switch to checksum 44 + out.writeAll(&self.dest.bytes) catch unreachable; 45 + out.writeAll(self.payload) catch unreachable; 46 + return out.buffered(); 47 + } 44 48 45 - pub const relay_dest_len = 4; 46 - 47 - pub fn parse(bytes: []const u8) MessageParseError!Message { 48 - var in: Reader = .fixed(bytes); 49 - const @"type" = in.takeEnum(std.meta.Tag(Message), .big) catch |err| switch (err) { 50 - error.InvalidEnumTag => return error.UnknownSaprusType, 51 - else => return error.InvalidMessage, 49 + // test toBytes { 50 + // var buf: [1024]u8 = undefined; 51 + // const relay: Relay = .init( 52 + // .fromBytes(&.{ 172, 18, 1, 30 }), 53 + // // zig fmt: off 54 + // &[_]u8{ 55 + // 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 56 + // 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 57 + // }, 58 + // // zig fmt: on 59 + // ); 60 + // // zig fmt: off 61 + // var expected = [_]u8{ 62 + // 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72, 63 + // 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 64 + // 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 65 + // }; 66 + // // zig fmt: on 67 + // try expectEqualMessageBuffers(&expected, relay.toBytes(&buf)); 68 + // } 52 69 }; 53 - const checksum = in.takeArray(2) catch return error.InvalidMessage; 54 - switch (@"type") { 55 - .relay => { 56 - const dest: Relay.Dest = .fromBytes( 57 - in.takeArray(relay_dest_len) catch return error.InvalidMessage, 58 - ); 59 - const payload = in.buffered(); 60 - return .{ 61 - .relay = .{ 62 - .dest = dest, 63 - .checksum = checksum.*, 64 - .payload = payload, 65 - }, 66 - }; 67 - }, 68 - .connection => { 69 - const src = in.takeInt(u16, .big) catch return error.InvalidMessage; 70 - const dest = in.takeInt(u16, .big) catch return error.InvalidMessage; 71 - const seq = in.takeInt(u32, .big) catch return error.InvalidMessage; 72 - const id = in.takeInt(u32, .big) catch return error.InvalidMessage; 73 - const reserved = in.takeByte() catch return error.InvalidMessage; 74 - const options = in.takeStruct(Connection.Options, .big) catch return error.InvalidMessage; 75 - const payload = in.buffered(); 76 - return .{ 77 - .connection = .{ 78 - .src = src, 79 - .dest = dest, 80 - .seq = seq, 81 - .id = id, 82 - .reserved = reserved, 83 - .options = options, 84 - .payload = payload, 85 - }, 86 - }; 87 - }, 88 - else => return error.NotImplementedSaprusType, 89 - } 90 - } 91 70 92 - test parse { 93 - _ = 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 }); 71 + pub const Connection = struct { 72 + src: u16, 73 + dest: u16, 74 + seq: u32, 75 + id: u32, 76 + reserved: u8 = undefined, 77 + options: Options = .{}, 78 + payload: []const u8, 94 79 95 - { 96 - const expected: Message = .{ 97 - .connection = .{ 98 - .src = 12416, 99 - .dest = 61680, 100 - .seq = 0, 101 - .id = 0, 102 - .reserved = 0, 103 - .options = @bitCast(@as(u8, 100)), 104 - .payload = &[_]u8{ 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 }, 105 - }, 80 + /// Option values. 81 + /// Currently used! 82 + pub const Options = packed struct(u8) { 83 + opt1: bool = false, 84 + opt2: bool = false, 85 + opt3: bool = false, 86 + opt4: bool = false, 87 + opt5: bool = false, 88 + opt6: bool = false, 89 + opt7: bool = false, 90 + management: bool = false, 106 91 }; 107 - 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 }); 108 92 109 - try std.testing.expectEqualDeep(expected, actual); 110 - } 111 - } 93 + /// Asserts that buf is large enough to fit the connection message. 94 + pub fn toBytes(self: Connection, buf: []u8) []u8 { 95 + var out: Writer = .fixed(buf); 96 + out.writeInt(u16, @intFromEnum(Message.connection), .big) catch unreachable; 97 + out.writeInt(u16, @intCast(self.payload.len + 14), .big) catch unreachable; // Saprus length field, unread. 98 + out.writeInt(u16, self.src, .big) catch unreachable; 99 + out.writeInt(u16, self.dest, .big) catch unreachable; 100 + out.writeInt(u32, self.seq, .big) catch unreachable; 101 + out.writeInt(u32, self.id, .big) catch unreachable; 102 + out.writeByte(self.reserved) catch unreachable; 103 + out.writeStruct(self.options, .big) catch unreachable; 104 + out.writeAll(self.payload) catch unreachable; 105 + return out.buffered(); 106 + } 112 107 113 - const Relay = struct { 114 - dest: Dest, 115 - checksum: [2]u8 = undefined, 116 - payload: []const u8, 108 + /// If the current message is a management message, return what kind. 109 + /// Else return null. 110 + pub fn management(self: Connection) ParseError!?Management { 111 + const b64_dec = std.base64.standard.Decoder; 112 + if (self.options.management) { 113 + var buf: [1]u8 = undefined; 114 + _ = b64_dec.decode(&buf, self.payload) catch return error.InvalidMessage; 117 115 118 - pub const Dest = struct { 119 - bytes: [relay_dest_len]u8, 116 + return switch (buf[0]) { 117 + 'P' => .ping, 118 + 'p' => .pong, 119 + else => error.UnknownSaprusType, 120 + }; 121 + } 122 + return null; 123 + } 120 124 121 - /// Asserts bytes is less than or equal to 4 bytes 122 - pub fn fromBytes(bytes: []const u8) Dest { 123 - var buf: [4]u8 = @splat(0); 124 - std.debug.assert(bytes.len <= buf.len); 125 - @memcpy(buf[0..bytes.len], bytes); 126 - return .{ .bytes = buf }; 127 - } 125 + pub const Management = enum { 126 + ping, 127 + pong, 128 + }; 128 129 }; 129 130 130 - pub fn init(dest: Dest, payload: []const u8) Relay { 131 - return .{ .dest = dest, .payload = payload }; 131 + pub fn toBytes(self: Message, buf: []u8) []u8 { 132 + return switch (self) { 133 + inline .relay, .connection => |m| m.toBytes(buf), 134 + else => unreachable, 135 + }; 132 136 } 133 137 134 - /// Asserts that buf is large enough to fit the relay message. 135 - pub fn toBytes(self: Relay, buf: []u8) []u8 { 136 - var out: Writer = .fixed(buf); 137 - out.writeInt(u16, @intFromEnum(Message.relay), .big) catch unreachable; 138 - out.writeInt(u16, @intCast(self.payload.len + 4), .big) catch unreachable; // Length field, but unread. Will switch to checksum 139 - out.writeAll(&self.dest.bytes) catch unreachable; 140 - out.writeAll(self.payload) catch unreachable; 141 - return out.buffered(); 142 - } 143 - 144 - test toBytes { 145 - var buf: [1024]u8 = undefined; 146 - const relay: Relay = .init( 147 - .fromBytes(&.{ 172, 18, 1, 30 }), 148 - // zig fmt: off 149 - &[_]u8{ 150 - 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 151 - 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 152 - }, 153 - // zig fmt: on 154 - ); 155 - // zig fmt: off 156 - var expected = [_]u8{ 157 - 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72, 158 - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 159 - 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 138 + pub fn parse(bytes: []const u8) ParseError!Message { 139 + var in: Reader = .fixed(bytes); 140 + const @"type" = in.takeEnum(std.meta.Tag(Message), .big) catch |err| switch (err) { 141 + error.InvalidEnumTag => return error.UnknownSaprusType, 142 + else => return error.InvalidMessage, 160 143 }; 161 - // zig fmt: on 162 - try expectEqualMessageBuffers(&expected, relay.toBytes(&buf)); 144 + const checksum = in.takeArray(2) catch return error.InvalidMessage; 145 + switch (@"type") { 146 + .relay => { 147 + const dest: Relay.Dest = .fromBytes( 148 + in.takeArray(relay_dest_len) catch return error.InvalidMessage, 149 + ); 150 + const payload = in.buffered(); 151 + return .{ 152 + .relay = .{ 153 + .dest = dest, 154 + .checksum = checksum.*, 155 + .payload = payload, 156 + }, 157 + }; 158 + }, 159 + .connection => { 160 + const src = in.takeInt(u16, .big) catch return error.InvalidMessage; 161 + const dest = in.takeInt(u16, .big) catch return error.InvalidMessage; 162 + const seq = in.takeInt(u32, .big) catch return error.InvalidMessage; 163 + const id = in.takeInt(u32, .big) catch return error.InvalidMessage; 164 + const reserved = in.takeByte() catch return error.InvalidMessage; 165 + const options = in.takeStruct(Connection.Options, .big) catch return error.InvalidMessage; 166 + const payload = in.buffered(); 167 + return .{ 168 + .connection = .{ 169 + .src = src, 170 + .dest = dest, 171 + .seq = seq, 172 + .id = id, 173 + .reserved = reserved, 174 + .options = options, 175 + .payload = payload, 176 + }, 177 + }; 178 + }, 179 + else => return error.NotImplementedSaprusType, 180 + } 163 181 } 164 - }; 165 182 166 - const Connection = struct { 167 - src: u16, 168 - dest: u16, 169 - seq: u32, 170 - id: u32, 171 - reserved: u8 = undefined, 172 - options: Options = .{}, 173 - payload: []const u8, 183 + test parse { 184 + _ = 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 }); 174 185 175 - /// Option values. 176 - /// Currently used! 177 - pub const Options = packed struct(u8) { 178 - opt1: bool = false, 179 - opt2: bool = false, 180 - opt3: bool = false, 181 - opt4: bool = false, 182 - opt5: bool = false, 183 - opt6: bool = false, 184 - opt7: bool = false, 185 - management: bool = false, 186 - }; 186 + { 187 + const expected: Message = .{ 188 + .connection = .{ 189 + .src = 12416, 190 + .dest = 61680, 191 + .seq = 0, 192 + .id = 0, 193 + .reserved = 0, 194 + .options = @bitCast(@as(u8, 100)), 195 + .payload = &[_]u8{ 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 }, 196 + }, 197 + }; 198 + 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 }); 187 199 188 - /// Asserts that buf is large enough to fit the connection message. 189 - pub fn toBytes(self: Connection, buf: []u8) []u8 { 190 - var out: Writer = .fixed(buf); 191 - out.writeInt(u16, @intFromEnum(Message.connection), .big) catch unreachable; 192 - out.writeInt(u16, @intCast(self.payload.len + 14), .big) catch unreachable; // Saprus length field, unread. 193 - out.writeInt(u16, self.src, .big) catch unreachable; 194 - out.writeInt(u16, self.dest, .big) catch unreachable; 195 - out.writeInt(u32, self.seq, .big) catch unreachable; 196 - out.writeInt(u32, self.id, .big) catch unreachable; 197 - out.writeByte(self.reserved) catch unreachable; 198 - out.writeStruct(self.options, .big) catch unreachable; 199 - out.writeAll(self.payload) catch unreachable; 200 - return out.buffered(); 200 + try std.testing.expectEqualDeep(expected, actual); 201 + } 201 202 } 202 203 203 - /// If the current message is a management message, return what kind. 204 - /// Else return null. 205 - pub fn management(self: Connection) MessageParseError!?Management { 206 - const b64_dec = std.base64.standard.Decoder; 207 - if (self.options.management) { 208 - var buf: [1]u8 = undefined; 209 - _ = b64_dec.decode(&buf, self.payload) catch return error.InvalidMessage; 210 - 211 - return switch (buf[0]) { 212 - 'P' => .ping, 213 - 'p' => .pong, 214 - else => error.UnknownSaprusType, 215 - }; 204 + test "Round trip" { 205 + { 206 + 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 }; 207 + const msg = (try parse(&expected)).connection; 208 + var res_buf: [expected.len + 1]u8 = undefined; // + 1 to test subslice result. 209 + const res = msg.toBytes(&res_buf); 210 + try expectEqualMessageBuffers(&expected, res); 216 211 } 217 - return null; 218 212 } 219 213 220 - pub const Management = enum { 221 - ping, 222 - pong, 214 + // Skip checking the length / checksum, because that is undefined. 215 + fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void { 216 + try std.testing.expectEqualSlices(u8, expected[0..2], actual[0..2]); 217 + try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]); 218 + } 219 + 220 + pub const TypeError = error{ 221 + NotImplementedSaprusType, 222 + UnknownSaprusType, 223 + }; 224 + pub const ParseError = TypeError || error{ 225 + InvalidMessage, 223 226 }; 224 227 }; 225 228 226 - test "Round trip" { 227 - { 228 - 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 }; 229 - const msg = (try parse(&expected)).connection; 230 - var res_buf: [expected.len + 1]u8 = undefined; // + 1 to test subslice result. 231 - const res = msg.toBytes(&res_buf); 232 - try expectEqualMessageBuffers(&expected, res); 233 - } 234 - } 235 - 236 - // Skip checking the length / checksum, because that is undefined. 237 - fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void { 238 - try std.testing.expectEqualSlices(u8, expected[0..2], actual[0..2]); 239 - try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]); 240 - } 229 + const relay_dest_len = 4; 241 230 242 231 const std = @import("std"); 243 232 const Allocator = std.mem.Allocator;
+8 -6
src/root.zig
··· 14 14 // You should have received a copy of the GNU General Public License along with 15 15 // Zaprus. If not, see <https://www.gnu.org/licenses/>. 16 16 17 + //! The Zaprus library is useful for implementing clients that interact with the [Saprus Protocol](https://gitlab.com/c2-games/red-team/saprus). 18 + //! 19 + //! The main entrypoint into this library is the `Client` type. 20 + //! It can be used to send fire and forget messages, and establish persistent connections. 21 + //! It is up to the consumer of this library to handle non-management message payloads. 22 + //! The library handles management messages automatically (right now, just ping). 23 + 17 24 pub const Client = @import("Client.zig"); 18 25 pub const Connection = @import("Connection.zig"); 19 - 20 - const msg = @import("message.zig"); 21 - 22 - pub const MessageTypeError = msg.MessageTypeError; 23 - pub const MessageParseError = msg.MessageParseError; 24 - pub const Message = msg.Message; 26 + pub const Message = @import("message.zig").Message; 25 27 26 28 test { 27 29 @import("std").testing.refAllDecls(@This());