11 Commits

Author SHA1 Message Date
4b09e13079 Release 0.2.2 2026-04-15 21:58:25 -04:00
91fdb2c6f8 refactor: use packed struct for IpAddr
This is more intuitive than using u32 directly, and follows the same pattern as the new MAC addr handling
2026-04-16 04:56:23 +03:00
7077aae9ce fix: convert MacAddr from vector to int
Still expose a vector / slice API with .fromSlice,
2026-04-16 04:56:23 +03:00
1f500b9b0a Remove any possible exit paths from main loop. 2026-04-08 22:40:58 -04:00
cbbd710b9d Release 0.2.1 2026-02-04 20:39:59 -05:00
974baf0274 Keep retrying if there is no interface 2026-02-05 01:38:24 +00:00
e744a44317 Remove test branch trigger 2026-02-04 12:18:45 -05:00
f202410f0d Include license and readme in build output
This is probably required for GPL compliance :-D
2026-02-03 23:06:30 -05:00
9dcbc44aa1 ReleaseSmall by default 2026-02-03 22:50:24 -05:00
d1fdf0c1be Automatically publish binaries to nextcloud
Updated the releases directory to use a specific account for releases
2026-02-03 22:44:53 -05:00
d1ca448835 Release v0.2.0 2026-02-01 21:02:39 -05:00
8 changed files with 132 additions and 21 deletions

View File

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

View File

@@ -3,4 +3,9 @@
This is an implementation of the [Saprus protocol](https://gitlab.com/c2-games/red-team/saprus) in Zig. 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. 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). Binary releases can be downloaded [here](https://cloud.zambito.xyz/s/7jJPTm68Zp3mN8F).
The code for this can be found here:
- https://tangled.org/zambyte.robbyzambito.me/zaprus
- https://git.robbyzambito.me/zaprus

View File

@@ -7,6 +7,9 @@ const std = @import("std");
// build runner to parallelize the build automatically (and the cache system to // build runner to parallelize the build automatically (and the cache system to
// know when a step doesn't need to be re-run). // know when a step doesn't need to be re-run).
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
// Ensure the license is included in the output directory
b.installFile("LICENSE.md", "LICENSE.md");
b.installFile("README.md", "README.md");
// Standard target options allow the person running `zig build` to choose // Standard target options allow the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which // what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options // means any target is allowed, and the default is native. Other options

View File

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

View File

@@ -46,11 +46,11 @@ pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void
const rand = io_source.interface(); const rand = io_source.interface();
var headers: EthIpUdp = .{ var headers: EthIpUdp = .{
.src_mac = self.socket.mac, .src_mac = .fromBytes(self.socket.mac),
.ip = .{ .ip = .{
.id = rand.int(u16), .id = rand.int(u16),
.src_addr = 0, //rand.int(u32), .src_addr = .fromBytes(.{ 0, 0, 0, 0 }), //rand.int(u32),
.dst_addr = @bitCast([_]u8{ 255, 255, 255, 255 }), .dst_addr = .fromBytes(.{ 255, 255, 255, 255 }),
.len = undefined, .len = undefined,
}, },
.udp = .{ .udp = .{
@@ -86,11 +86,11 @@ pub fn connect(self: Client, io: Io, payload: []const u8) (error{ BpfAttachFaile
const rand = io_source.interface(); const rand = io_source.interface();
var headers: EthIpUdp = .{ var headers: EthIpUdp = .{
.src_mac = self.socket.mac, .src_mac = .fromBytes(self.socket.mac),
.ip = .{ .ip = .{
.id = rand.int(u16), .id = rand.int(u16),
.src_addr = 0, //rand.int(u32), .src_addr = .fromBytes(.{ 0, 0, 0, 0 }), //rand.int(u32),
.dst_addr = @bitCast([_]u8{ 255, 255, 255, 255 }), .dst_addr = .fromBytes(.{ 255, 255, 255, 255 }),
.len = undefined, .len = undefined,
}, },
.udp = .{ .udp = .{

View File

@@ -14,6 +14,28 @@
// 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 IpAddr = packed struct {
int: I,
const V = @Vector(4, u8);
const I = u32;
pub fn fromBytes(s: V) IpAddr {
return .{ .int = @bitCast(s) };
}
};
pub const MacAddr = packed struct {
int: I,
const V = @Vector(6, u8);
const I = @Int(.unsigned, @bitSizeOf(V));
pub fn fromBytes(s: V) MacAddr {
return .{ .int = @bitCast(s) };
}
};
pub const EthIpUdp = packed struct(u336) { // 42 bytes * 8 bits = 336 pub const EthIpUdp = packed struct(u336) { // 42 bytes * 8 bits = 336
// --- UDP (Last in memory, defined first for LSB->MSB) --- // --- UDP (Last in memory, defined first for LSB->MSB) ---
udp: packed struct { udp: packed struct {
@@ -25,8 +47,8 @@ pub const EthIpUdp = packed struct(u336) { // 42 bytes * 8 bits = 336
// --- IP --- // --- IP ---
ip: packed struct { ip: packed struct {
dst_addr: u32, dst_addr: IpAddr,
src_addr: u32, src_addr: IpAddr,
header_checksum: u16 = 0, header_checksum: u16 = 0,
protocol: u8 = 17, // udp protocol: u8 = 17, // udp
ttl: u8 = 0x40, ttl: u8 = 0x40,
@@ -53,8 +75,8 @@ pub const EthIpUdp = packed struct(u336) { // 42 bytes * 8 bits = 336
// --- Ethernet --- // --- Ethernet ---
eth_type: u16 = std.os.linux.ETH.P.IP, eth_type: u16 = std.os.linux.ETH.P.IP,
src_mac: @Vector(6, u8), src_mac: MacAddr,
dst_mac: @Vector(6, u8) = @splat(0xff), dst_mac: MacAddr = .fromBytes(@splat(0xff)),
pub fn toBytes(self: @This()) [336 / 8]u8 { pub fn toBytes(self: @This()) [336 / 8]u8 {
var res: [336 / 8]u8 = undefined; var res: [336 / 8]u8 = undefined;

View File

@@ -100,7 +100,7 @@ pub fn init() error{
}; };
} }
pub fn setTimeout(self: *RawSocket, sec: isize, usec: i64) !void { pub fn setTimeout(self: *RawSocket, sec: isize, usec: i64) error{SetTimeoutError}!void {
const timeout: std.os.linux.timeval = .{ .sec = sec, .usec = usec }; const timeout: std.os.linux.timeval = .{ .sec = sec, .usec = usec };
const timeout_ret = std.os.linux.setsockopt(self.fd, std.os.linux.SOL.SOCKET, std.os.linux.SO.RCVTIMEO, @ptrCast(&timeout), @sizeOf(@TypeOf(timeout))); const timeout_ret = std.os.linux.setsockopt(self.fd, std.os.linux.SOL.SOCKET, std.os.linux.SO.RCVTIMEO, @ptrCast(&timeout), @sizeOf(@TypeOf(timeout)));
if (timeout_ret != 0) return error.SetTimeoutError; if (timeout_ret != 0) return error.SetTimeoutError;

View File

@@ -146,28 +146,54 @@ pub fn main(init: std.process.Init) !void {
if (flags.connect != null) { if (flags.connect != null) {
reconnect: while (true) { reconnect: while (true) {
client = try .init(); client = SaprusClient.init() catch |err| switch (err) {
error.NoInterfaceFound => {
init.io.sleep(.fromMilliseconds(100), .boot) catch unreachable;
continue :reconnect;
},
else => |e| return e,
};
defer client.deinit(); defer client.deinit();
log.debug("Starting connection", .{}); log.debug("Starting connection", .{});
try client.socket.setTimeout(if (is_debug) 3 else 25, 0); client.socket.setTimeout(if (is_debug) 3 else 25, 0) catch {
log.err("Unable to set timeout", .{});
init.io.sleep(.fromMilliseconds(100), .boot) catch unreachable;
continue :reconnect;
};
var connection = client.connect(init.io, w.buffered()) catch { var connection = client.connect(init.io, w.buffered()) catch {
log.debug("Connection timed out", .{}); log.debug("Connection timed out", .{});
continue; continue :reconnect;
}; };
log.debug("Connection started", .{}); log.debug("Connection started", .{});
next_message: while (true) { next_message: while (true) {
var res_buf: [2048]u8 = undefined; var res_buf: [2048]u8 = undefined;
try client.socket.setTimeout(if (is_debug) 60 else 600, 0); client.socket.setTimeout(if (is_debug) 60 else 600, 0) catch {
log.err("Unable to set timeout", .{});
init.io.sleep(.fromMilliseconds(100), .boot) catch unreachable;
continue :reconnect;
};
const next = connection.next(init.io, &res_buf) catch { const next = connection.next(init.io, &res_buf) catch {
continue :reconnect; continue :reconnect;
}; };
const b64d = std.base64.standard.Decoder; const b64d = std.base64.standard.Decoder;
var connection_payload_buf: [2048]u8 = undefined; var connection_payload_buf: [2048]u8 = undefined;
const connection_payload = connection_payload_buf[0..try b64d.calcSizeForSlice(next)]; const connection_payload = blk: {
const size = b64d.calcSizeForSlice(next) catch |err| switch (err) {
error.InvalidCharacter, error.InvalidPadding => {
log.warn("Invalid base64 message received, ignoring: '{s}'", .{next});
continue :next_message;
},
error.NoSpaceLeft => {
log.warn("No space left when decoding base64 string, ignoring.", .{});
continue :next_message;
},
};
break :blk connection_payload_buf[0..size];
};
b64d.decode(connection_payload, next) catch { b64d.decode(connection_payload, next) catch {
log.debug("Failed to decode message, skipping: '{s}'", .{connection_payload}); log.debug("Failed to decode message, skipping: '{s}'", .{connection_payload});
continue; continue;
@@ -215,7 +241,7 @@ pub fn main(init: std.process.Init) !void {
var is_killed: std.atomic.Value(bool) = .init(false); var is_killed: std.atomic.Value(bool) = .init(false);
var kill_task = try init.io.concurrent(killProcessAfter, .{ init.io, &child, .fromSeconds(3), &is_killed }); var kill_task = init.io.concurrent(killProcessAfter, .{ init.io, &child, .fromSeconds(3), &is_killed }) catch unreachable;
defer _ = kill_task.cancel(init.io) catch {}; defer _ = kill_task.cancel(init.io) catch {};
var cmd_output_buf: [SaprusClient.max_payload_len * 2]u8 = undefined; var cmd_output_buf: [SaprusClient.max_payload_len * 2]u8 = undefined;
@@ -238,12 +264,41 @@ pub fn main(init: std.process.Init) !void {
break; break;
}, },
}; };
cmd_output.print("{b64}", .{try child_output_reader.interface.takeArray(child_output_buf.len)}) catch unreachable; const child_output_chunk = child_output_reader.interface.takeArray(child_output_buf.len) catch |err| switch (err) {
error.EndOfStream => {
log.warn("Reached end of stream when reading from the child process. Maybe this should be handled more gracefull, but ignoring for now.", .{});
continue :next_message;
},
error.ReadFailed => if (child_output_reader.err) |co_err| switch (co_err) {
error.AccessDenied,
error.ConnectionResetByPeer,
error.InputOutput,
error.IsDir,
error.LockViolation,
error.NotOpenForReading,
error.SocketUnconnected,
error.SystemResources,
error.WouldBlock,
=> |e| {
log.err("Unending error reading output from child process: {t}", .{e});
continue :next_message;
},
error.Canceled => |e| return e,
error.Unexpected => {
log.err("Unexpected error reading output from child process.", .{});
continue :next_message;
},
} else {
log.err("Shouldn't get here :(", .{});
continue :next_message;
},
};
cmd_output.print("{b64}", .{child_output_chunk}) 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;
}; };
try init.io.sleep(.fromMilliseconds(40), .boot); init.io.sleep(.fromMilliseconds(40), .boot) catch unreachable;
} else { } else {
kill_task.cancel(init.io) catch {}; kill_task.cancel(init.io) catch {};
killProcessAfter(init.io, &child, .zero, &is_killed) catch |err| { killProcessAfter(init.io, &child, .zero, &is_killed) catch |err| {