From 72ba68744eb7fe5fb4c03759f1faca8e41a683b1 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Fri, 5 Jul 2024 20:38:10 +0200 Subject: [PATCH] Regex free process reader --- build.zig | 15 ++++ src/clog.zig | 1 - src/main.zig | 1 + src/proc.zig | 28 ++++--- src/process.zig | 184 ++++++++++++++++++++++++++++++++++++++++++++++ src/protocols.zig | 11 --- test/proc1/tcp | 8 ++ test/proc1/tcp6 | 7 ++ test/proc1/udp | 6 ++ test/proc1/udp6 | 5 ++ 10 files changed, 244 insertions(+), 22 deletions(-) delete mode 100644 src/clog.zig create mode 100644 src/process.zig delete mode 100644 src/protocols.zig create mode 100644 test/proc1/tcp create mode 100644 test/proc1/tcp6 create mode 100644 test/proc1/udp create mode 100644 test/proc1/udp6 diff --git a/build.zig b/build.zig index 346e3c7..450eebb 100644 --- a/build.zig +++ b/build.zig @@ -77,6 +77,20 @@ pub fn build(b: *std.Build) void { const run_procnet_unit_tests = b.addRunArtifact(procnet_unit_tests); run_procnet_unit_tests.has_side_effects = true; // needed so tests aren't cached + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const process_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/process.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + process_unit_tests.addIncludePath(.{ .path = "lib" }); + process_unit_tests.linkLibrary(relib); + + const run_process_unit_tests = b.addRunArtifact(process_unit_tests); + run_process_unit_tests.has_side_effects = true; // needed so tests aren't cached + const exe_unit_tests = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = target, @@ -92,5 +106,6 @@ pub fn build(b: *std.Build) void { // running the unit tests. const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_procnet_unit_tests.step); + test_step.dependOn(&run_process_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); } diff --git a/src/clog.zig b/src/clog.zig deleted file mode 100644 index 95a0b68..0000000 --- a/src/clog.zig +++ /dev/null @@ -1 +0,0 @@ -const std = @import("std"); diff --git a/src/main.zig b/src/main.zig index 4b11185..f943407 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const proc = @import("procnet.zig"); const pid = @import("procpid.zig"); const clog = @import("proc.zig"); +const proces = @import("process.zig"); const c = @cImport({ @cInclude("arpa/inet.h"); @cInclude("signal.h"); diff --git a/src/proc.zig b/src/proc.zig index 3c5667e..d3765b3 100644 --- a/src/proc.zig +++ b/src/proc.zig @@ -5,7 +5,7 @@ const std = @import("std"); -// Socket that currently clogs a port with protocol information +/// Socket that currently clogs a port with protocol information pub const ClogSocket = struct { port: u16, inode: std.posix.ino_t, @@ -13,7 +13,7 @@ pub const ClogSocket = struct { protocol_data: ProtocolData, }; -// Known protocols +/// Known protocols pub const Protocol = enum { udp_v4, udp_v6, @@ -21,7 +21,7 @@ pub const Protocol = enum { tcp_v6, }; -// Protocol specific data +/// Protocol specific data pub const ProtocolData = union(Protocol) { udp_v4: struct { addr: u32 }, udp_v6: struct { addr: u128 }, @@ -29,15 +29,17 @@ pub const ProtocolData = union(Protocol) { tcp_v6: struct { addr: u128 }, }; -// Parses `/proc/net` information into a flat list of ClogSockets. -// -// Memory is owned by caller. []ClogSocket must be freed by caller. +/// Parses `/proc/net` information into a flat list of ClogSockets. +/// +/// Memory is owned by caller. []ClogSocket must be freed by caller. +/// +/// If proc_path is `null` will default to `/proc/net` pub fn parse(alloc: std.mem.Allocator, proc_path: ?[]u8) ![]ClogSocket { + const base = proc_path orelse "/proc/net"; + var buf = std.ArrayList(ClogSocket).init(alloc); defer buf.deinit(); // noop we re-own memory to parent at the end - const base = proc_path orelse "/proc/net"; - var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const child_alloc = arena.allocator(); @@ -133,6 +135,12 @@ fn parse_internal(path: []const u8, protocol: Protocol, buf: *std.ArrayList(Clog test "parse" { var alloc = std.testing.allocator; - const clogs = try parse(alloc, null); - alloc.free(clogs); + + std.testing.log_level = .info; + + const file_path = try std.fs.cwd().realpathAlloc(alloc, "./test/proc1"); + defer alloc.free(file_path); + + const clogs = try parse(alloc, file_path); + defer alloc.free(clogs); } diff --git a/src/process.zig b/src/process.zig new file mode 100644 index 0000000..1e86447 --- /dev/null +++ b/src/process.zig @@ -0,0 +1,184 @@ +//! Find and get information about a processes + +const std = @import("std"); + +pub const Clogs = struct { + items: []ClogProcess, + alloc: std.mem.Allocator, + + pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("{any}", .{value.items}); + } + + fn deinit(self: @This()) void { + for (self.items) |item| { + item.deinit(); + } + self.alloc.free(self.items); + } +}; + +pub const ClogProcess = struct { + // central data + pid: std.posix.pid_t, + inode: std.posix.ino_t, + fd: std.posix.fd_t, + comm: []u8, + exe: []u8, + cmdline: [][]u8, + + // for housekeeping + alloc: std.mem.Allocator, + + pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll("\nClogProcess{\n"); + try writer.print("\t.pid={d}, .inode={d}, .fd={d},\n", .{ value.pid, value.inode, value.fd }); + try writer.print("\t.comm={s}\n", .{value.comm}); + try writer.print("\t.exe={s}\n", .{value.exe}); + try writer.print("\t.cmdline={s}\n", .{value.cmdline}); + try writer.writeAll("}"); + } + + fn deinit(self: @This()) void { + self.alloc.free(self.comm); + self.alloc.free(self.exe); + for (self.cmdline) |cl| { + self.alloc.free(cl); + } + self.alloc.free(self.cmdline); + } +}; + +pub fn find_by_inode(alloc: std.mem.Allocator, inode: std.posix.ino_t, proc_path: ?[]u8) !Clogs { + const base = proc_path orelse "/proc"; + + var clogs = std.ArrayList(ClogProcess).init(alloc); + defer clogs.deinit(); // noop we re-own memory to parent at the end + + const proc_dir = try std.fs.openDirAbsolute(base, .{ .iterate = true }); + + var process_it = PidIterator{ .it = AccessSafeIterator{ .it = proc_dir.iterate() } }; + while (try process_it.next()) |process_entry| { + const process_dir = proc_dir.openDir(process_entry.name, .{ .iterate = true }) catch |err| switch (err) { + error.AccessDenied => continue, + else => return err, + }; + const fd_dir = process_dir.openDir("fd", .{ .iterate = true }) catch |err| switch (err) { + error.AccessDenied => continue, + else => return err, + }; + + var fd_it = FdIterator{ .it = AccessSafeIterator{ .it = fd_dir.iterate() } }; + fdit: while (try fd_it.next()) |fd_entry| { + _ = std.fmt.parseInt(std.posix.pid_t, fd_entry.name, 10) catch continue; + const fd_stat = fd_dir.statFile(fd_entry.name) catch |err| switch (err) { + error.AccessDenied => continue, + else => return err, + }; + + if (fd_stat.inode == inode) { + std.log.debug("Found inode {d} in {s}/{s}", .{ fd_stat.inode, process_entry.name, fd_entry.name }); + + const pid = try std.fmt.parseInt(std.posix.pid_t, process_entry.name, 10); + for (clogs.items) |clog| { + if (clog.pid == pid) { + std.log.debug("pid {d} already in clogs", .{pid}); + continue :fdit; + } + } + + var comm_file = process_dir.openFile("comm", .{}) catch |err| switch (err) { + error.AccessDenied => continue, + else => return err, + }; + var cmdline_file = process_dir.openFile("cmdline", .{}) catch |err| switch (err) { + error.AccessDenied => continue, + else => return err, + }; + + var exe_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const exe_path = process_dir.readLink("exe", &exe_path_buf) catch |err| switch (err) { + error.AccessDenied => continue, + else => return err, + }; + + const comm = try comm_file.readToEndAlloc(alloc, 4096); + defer alloc.free(comm); + + var cmdline = std.ArrayList([]u8).init(alloc); + const cmdline_raw = try cmdline_file.readToEndAlloc(alloc, 4096); + defer alloc.free(cmdline_raw); + var cmdline_it = std.mem.splitScalar(u8, cmdline_raw, 0x0); + while (cmdline_it.next()) |cl| { + try cmdline.append(try alloc.dupe(u8, std.mem.trim(u8, cl, "\n"))); + } + + try clogs.append(ClogProcess{ + .pid = pid, + .inode = fd_stat.inode, + .fd = try std.fmt.parseInt(std.posix.pid_t, fd_entry.name, 10), + .comm = try alloc.dupe(u8, std.mem.trim(u8, comm, "\n")), + .cmdline = try cmdline.toOwnedSlice(), + .exe = try alloc.dupe(u8, exe_path), + .alloc = alloc, + }); + } + } + } + + return Clogs{ .items = try clogs.toOwnedSlice(), .alloc = alloc }; +} + +const FdIterator = struct { + it: AccessSafeIterator, + + pub fn next(self: *@This()) !?std.fs.Dir.Entry { + return try self.it.next(); + } +}; + +/// Iterator over directories that look like a process directory +const PidIterator = struct { + it: AccessSafeIterator, + + pub fn next(self: *@This()) !?std.fs.Dir.Entry { + while (try self.it.next()) |entry| { + if (entry.kind == .directory) { + _ = std.fmt.parseInt(std.posix.pid_t, entry.name, 10) catch continue; + return entry; + } + } + + return null; + } +}; + +/// Iterator which ignores `AccessDenied` errors from child iterator +const AccessSafeIterator = struct { + it: std.fs.Dir.Iterator, + + pub fn next(self: *@This()) !?std.fs.Dir.Entry { + while (true) { + const elem = self.it.next() catch |err| switch (err) { + error.AccessDenied => continue, + else => return err, + }; + + if (elem) |e| { + self.it.dir.access(e.name, .{}) catch continue; + } + + return elem; + } + + return null; + } +}; + +test "simple" { + std.testing.log_level = .info; + + const clogs = try find_by_inode(std.testing.allocator, 3568, null); + defer clogs.deinit(); + std.log.info("clogs: {any}", .{clogs}); +} diff --git a/src/protocols.zig b/src/protocols.zig deleted file mode 100644 index 315adfe..0000000 --- a/src/protocols.zig +++ /dev/null @@ -1,11 +0,0 @@ -const std = @import("std"); - -pub const Protocol { - proto: enum { - TCP_V4, - TCP_V6, - UDP_V4, - UDP_V6 - }, - port_map: std.AutoHashMap(u16, Process); -}; diff --git a/test/proc1/tcp b/test/proc1/tcp new file mode 100644 index 0000000..e19f054 --- /dev/null +++ b/test/proc1/tcp @@ -0,0 +1,8 @@ + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7987 1 00000000510949ee 99 0 0 10 0 + 1: 0100007F:0521 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 229198 1 000000000cc7de62 99 0 0 10 0 + 2: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 12004 1 000000000e13fb3e 99 0 0 10 5 + 3: 9600A8C0:91BE 1A71528C:01BB 01 00000000:00000000 02:00005772 00000000 1000 0 312207 2 0000000039cb8c9e 61 3 31 10 -1 + 4: 9600A8C0:8CF0 5DF36B22:01BB 01 00000000:00000000 02:00001FA2 00000000 1000 0 264134 2 00000000a051e7c1 22 3 31 10 -1 + 5: 9600A8C0:B0A6 AC4C825E:01BB 01 00000000:00000000 02:00097528 00000000 1000 0 304874 2 000000009579cfcc 22 3 30 10 -1 + 6: 9600A8C0:D042 AC4C825E:01BB 01 00000000:00000000 02:0005F59F 00000000 1000 0 279726 2 00000000b9879d52 22 3 24 10 -1 diff --git a/test/proc1/tcp6 b/test/proc1/tcp6 new file mode 100644 index 0000000..579c2a4 --- /dev/null +++ b/test/proc1/tcp6 @@ -0,0 +1,7 @@ + sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 00000000000000000000000001000000:0277 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7986 1 00000000a819e168 99 0 0 10 0 + 1: 0000000000000000FFFF00000100007F:CE5D 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 44943 1 000000009005f71a 99 0 0 10 0 + 2: 8883022A00E0C11A96B7CA3E649815D5:9B08 00470626000000000000000034B11368:01BB 06 00000000:00000000 03:00001606 00000000 0 0 0 3 00000000f49b65c7 + 3: 8883022A00E0C11A96B7CA3E649815D5:9A5A 7C060120F02001140000000001000000:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 291092 1 0000000038d2a6ed 23 3 30 10 -1 + 4: 8883022A00E0C11A96B7CA3E649815D5:E52E 5014002A0C080D40000000000E200000:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 320332 1 00000000ec54adc7 23 3 31 10 -1 + 5: 8883022A00E0C11A96B7CA3E649815D5:A0BA C011002A000066000000000033000000:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 306175 1 00000000e41ac297 23 3 28 10 -1 diff --git a/test/proc1/udp b/test/proc1/udp new file mode 100644 index 0000000..846f37f --- /dev/null +++ b/test/proc1/udp @@ -0,0 +1,6 @@ + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +17624: 017AA8C0:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 12003 2 000000000be6b861 0 +17638: 00000000:0043 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 12000 2 00000000183b2711 0 +17639: 9600A8C0:0044 0100A8C0:0043 01 00000000:00000000 00:00000000 00000000 0 0 276252 2 000000000a09b1ba 0 +22924: 00000000:14E9 00000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3352 2 000000002c689e03 0 +31811: 00000000:B7A0 00000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3354 2 0000000071428bb3 0 diff --git a/test/proc1/udp6 b/test/proc1/udp6 new file mode 100644 index 0000000..f9e2eb0 --- /dev/null +++ b/test/proc1/udp6 @@ -0,0 +1,5 @@ + sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +18117: 000080FE000000000716539EF9245F4B:0222 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 274919 2 00000000503fce27 0 +20307: 00000000000000000000000000000000:8AB0 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3355 2 00000000e8a66559 0 +22924: 00000000000000000000000000000000:14E9 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3353 2 00000000f71f77f1 0 +30065: 00000000000000000000000000000000:B0CE 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 316229 2 000000008792aef6 640