2 * sdjournal is an extcap tool used to dump systemd journal entries.
4 * Adapted from sshdump.
5 * Copyright 2018, Gerald Combs and Dario Lombardo
7 * Wireshark - Network traffic analyzer
8 * By Gerald Combs <gerald@wireshark.org>
9 * Copyright 1998 Gerald Combs
11 * SPDX-License-Identifier: GPL-2.0-or-later
16 * - Add an option for sd_journal_open flags, e.g. SD_JOURNAL_LOCAL_ONLY.
17 * - Add journalctl options - --boot, --machine, --directory, etc.
21 #define WS_LOG_DOMAIN "sdjournal"
23 #include <extcap/extcap-base.h>
24 #include <wsutil/interface.h>
25 #include <wsutil/file_util.h>
26 #include <wsutil/filesystem.h>
27 #include <wsutil/privileges.h>
28 #include <wsutil/wslog.h>
29 #include <writecap/pcapio.h>
30 #include <wiretap/wtap.h>
32 #include <systemd/sd-journal.h>
33 #include <systemd/sd-id128.h>
39 #define SDJOURNAL_VERSION_MAJOR "1"
40 #define SDJOURNAL_VERSION_MINOR "0"
41 #define SDJOURNAL_VERSION_RELEASE "0"
43 #define SDJOURNAL_EXTCAP_INTERFACE "sdjournal"
44 #define BLOCK_TYPE_SYSTEMD_JOURNAL_EXPORT 0x00000009
47 EXTCAP_BASE_OPTIONS_ENUM
,
53 static const struct ws_option longopts
[] = {
55 { "help", ws_no_argument
, NULL
, OPT_HELP
},
56 { "version", ws_no_argument
, NULL
, OPT_VERSION
},
57 { "start-from", ws_required_argument
, NULL
, OPT_START_FROM
},
61 #define FLD_BOOT_ID "_BOOT_ID="
62 #define FLD_BOOT_ID_LEN (8 + 1 + 33 + 1)
64 // The Journal Export Format specification doesn't place limits on entry
65 // lengths or lines per entry. We do.
66 #define ENTRY_BUF_LENGTH WTAP_MAX_PACKET_SIZE_STANDARD
67 #define MAX_EXPORT_ENTRY_LENGTH (ENTRY_BUF_LENGTH - 4 - 4 - 4) // Block type - total length - total length
69 static int sdj_dump_entries(sd_journal
*jnl
, FILE* fp
)
71 int ret
= EXIT_SUCCESS
;
72 uint8_t *entry_buff
= g_new(uint8_t, ENTRY_BUF_LENGTH
);
76 * Read journal entries and write them as packets. Output must
77 * match `journalctl --output=export`.
81 uint64_t pkt_rt_ts
, mono_ts
;
83 char boot_id_str
[FLD_BOOT_ID_LEN
] = FLD_BOOT_ID
;
84 uint32_t block_type
= BLOCK_TYPE_SYSTEMD_JOURNAL_EXPORT
;
85 uint32_t data_end
= 8; // Block type + total length
88 uint64_t bytes_written
= 0;
91 memcpy(entry_buff
, &block_type
, 4);
93 jr
= sd_journal_next(jnl
);
94 ws_debug("sd_journal_next: %d", jr
);
96 ws_warning("Error fetching journal entry: %s", g_strerror(jr
));
99 sd_journal_wait(jnl
, (uint64_t) -1);
103 jr
= sd_journal_get_cursor(jnl
, &cursor
);
105 ws_warning("Error fetching cursor: %s", g_strerror(jr
));
108 data_end
+= snprintf(entry_buff
+data_end
, MAX_EXPORT_ENTRY_LENGTH
-data_end
, "__CURSOR=%s\n", cursor
);
111 jr
= sd_journal_get_realtime_usec(jnl
, &pkt_rt_ts
);
113 ws_warning("Error fetching realtime timestamp: %s", g_strerror(jr
));
116 data_end
+= snprintf(entry_buff
+data_end
, MAX_EXPORT_ENTRY_LENGTH
-data_end
, "__REALTIME_TIMESTAMP=%" PRIu64
"\n", pkt_rt_ts
);
118 jr
= sd_journal_get_monotonic_usec(jnl
, &mono_ts
, &boot_id
);
120 ws_warning("Error fetching monotonic timestamp: %s", g_strerror(jr
));
123 sd_id128_to_string(boot_id
, boot_id_str
+ strlen(FLD_BOOT_ID
));
124 data_end
+= snprintf(entry_buff
+data_end
, MAX_EXPORT_ENTRY_LENGTH
-data_end
, "__MONOTONIC_TIMESTAMP=%" PRIu64
"\n%s\n", mono_ts
, boot_id_str
);
125 ws_debug("Entry header is %u bytes", data_end
);
127 SD_JOURNAL_FOREACH_DATA(jnl
, fld_data
, fld_len
) {
128 uint8_t *eq_ptr
= (uint8_t *) memchr(fld_data
, '=', fld_len
);
130 ws_warning("Invalid field.");
133 if (g_utf8_validate((const char *) fld_data
, (ssize_t
) fld_len
, NULL
)) {
134 // Allow for two trailing newlines, one here and one
135 // at the end of the buffer.
136 if (fld_len
> MAX_EXPORT_ENTRY_LENGTH
-data_end
-2) {
137 ws_debug("Breaking on UTF-8 field: %u + %zd", data_end
, fld_len
);
140 memcpy(entry_buff
+data_end
, fld_data
, fld_len
);
141 data_end
+= (uint32_t) fld_len
;
142 entry_buff
[data_end
] = '\n';
145 // \n + 64-bit size + \n + trailing \n = 11
146 if (fld_len
> MAX_EXPORT_ENTRY_LENGTH
-data_end
-11) {
147 ws_debug("Breaking on binary field: %u + %zd", data_end
, fld_len
);
150 ptrdiff_t name_len
= eq_ptr
- (const uint8_t *) fld_data
;
151 uint64_t le_data_len
;
152 le_data_len
= htole64(fld_len
- name_len
- 1);
153 memcpy(entry_buff
+data_end
, fld_data
, name_len
);
155 entry_buff
[data_end
] = '\n';
157 memcpy(entry_buff
+data_end
, &le_data_len
, 8);
159 memcpy(entry_buff
+data_end
, (const uint8_t *) fld_data
+ name_len
+ 1, fld_len
- name_len
);
160 data_end
+= fld_len
- name_len
;
165 size_t pad_len
= 4 - (data_end
% 4);
166 memset(entry_buff
+data_end
, '\0', pad_len
);
170 uint32_t total_len
= data_end
+ 4;
171 memcpy (entry_buff
+4, &total_len
, 4);
172 memcpy (entry_buff
+data_end
, &total_len
, 4);
174 ws_debug("Attempting to write %u bytes", total_len
);
175 if (!pcapng_write_block(fp
, entry_buff
, total_len
, &bytes_written
, &err
)) {
176 ws_warning("Can't write event: %s", strerror(err
));
189 static int sdj_start_export(const int start_from_entries
, const bool start_from_end
, const char* fifo
)
192 uint64_t bytes_written
= 0;
194 sd_journal
*jnl
= NULL
;
196 char boot_id_str
[FLD_BOOT_ID_LEN
] = FLD_BOOT_ID
;
197 int ret
= EXIT_FAILURE
;
198 char* err_info
= NULL
;
203 if (g_strcmp0(fifo
, "-")) {
204 /* Open or create the output file */
205 fp
= fopen(fifo
, "wb");
207 ws_warning("Error creating output file: %s (%s)", fifo
, g_strerror(errno
));
213 appname
= ws_strdup_printf(SDJOURNAL_EXTCAP_INTERFACE
" (Wireshark) %s.%s.%s",
214 SDJOURNAL_VERSION_MAJOR
, SDJOURNAL_VERSION_MINOR
, SDJOURNAL_VERSION_RELEASE
);
215 success
= pcapng_write_section_header_block(fp
,
220 -1, /* section_length */
226 ws_warning("Can't write pcapng file header");
230 jr
= sd_journal_open(&jnl
, 0);
232 ws_warning("Error opening journal: %s", g_strerror(jr
));
236 jr
= sd_id128_get_boot(&boot_id
);
238 ws_warning("Error fetching system boot ID: %s", g_strerror(jr
));
242 sd_id128_to_string(boot_id
, boot_id_str
+ strlen(FLD_BOOT_ID
));
243 jr
= sd_journal_add_match(jnl
, boot_id_str
, strlen(boot_id_str
));
245 ws_warning("Error adding match: %s", g_strerror(jr
));
249 // According to the documentation, fields *might be* truncated to 64K.
250 // Let's assume that 2048 is a good balance between fetching entire fields
251 // and being able to fit as many fields as possible into a packet.
252 sd_journal_set_data_threshold(jnl
, 2048);
254 if (start_from_end
) {
255 ws_debug("Attempting to seek %d entries from the end", start_from_entries
);
256 jr
= sd_journal_seek_tail(jnl
);
258 ws_warning("Error starting at end: %s", g_strerror(jr
));
261 jr
= sd_journal_previous_skip(jnl
, (uint64_t) start_from_entries
+ 1);
263 ws_warning("Error skipping backward: %s", g_strerror(jr
));
267 ws_debug("Attempting to seek %d entries from the beginning", start_from_entries
);
268 jr
= sd_journal_seek_head(jnl
);
270 ws_warning("Error starting at beginning: %s", g_strerror(jr
));
273 if (start_from_entries
> 0) {
274 jr
= sd_journal_next_skip(jnl
, (uint64_t) start_from_entries
);
276 ws_warning("Error skipping forward: %s", g_strerror(jr
));
282 /* read from channel and write into fp */
283 if (sdj_dump_entries(jnl
, fp
) != 0) {
284 ws_warning("Error dumping entries");
292 sd_journal_close(jnl
);
296 ws_warning("%s", err_info
);
301 /* clean up and exit */
302 if (g_strcmp0(fifo
, "-")) {
308 static int list_config(char *interface
)
313 ws_warning("ERROR: No interface specified.");
317 if (g_strcmp0(interface
, SDJOURNAL_EXTCAP_INTERFACE
)) {
318 ws_warning("ERROR: interface must be %s", SDJOURNAL_EXTCAP_INTERFACE
);
322 printf("arg {number=%u}{call=--start-from}{display=Starting position}"
323 "{type=string}{tooltip=The journal starting position. Values "
324 "with a leading \"+\" start from the beginning, similar to the "
325 "\"tail\" command}{required=false}{group=Journal}\n", inc
++);
327 extcap_config_debug(&inc
);
332 int main(int argc
, char **argv
)
334 char* configuration_init_error
;
337 int start_from_entries
= 10;
338 bool start_from_end
= true;
339 int ret
= EXIT_FAILURE
;
340 extcap_parameters
* extcap_conf
= g_new0(extcap_parameters
, 1);
342 char* help_header
= NULL
;
344 /* Initialize log handler early so we can have proper logging during startup. */
345 extcap_log_init("sdjournal");
348 * Get credential information for later use.
350 init_process_policies();
353 * Attempt to get the pathname of the directory containing the
356 configuration_init_error
= configuration_init(argv
[0], NULL
);
357 if (configuration_init_error
!= NULL
) {
358 ws_warning("Can't get pathname of directory containing the extcap program: %s.",
359 configuration_init_error
);
360 g_free(configuration_init_error
);
363 help_url
= data_file_url("sdjournal.html");
364 extcap_base_set_util_info(extcap_conf
, argv
[0], SDJOURNAL_VERSION_MAJOR
, SDJOURNAL_VERSION_MINOR
,
365 SDJOURNAL_VERSION_RELEASE
, help_url
);
367 // We don't have an SDJOURNAL DLT, so use USER0 (147).
368 extcap_base_register_interface(extcap_conf
, SDJOURNAL_EXTCAP_INTERFACE
, "systemd Journal Export", 147, "USER0");
370 help_header
= ws_strdup_printf(
371 " %s --extcap-interfaces\n"
372 " %s --extcap-interface=%s --extcap-dlts\n"
373 " %s --extcap-interface=%s --extcap-config\n"
374 " %s --extcap-interface=%s --start-from=+0 --fifo=FILENAME --capture\n",
376 argv
[0], SDJOURNAL_EXTCAP_INTERFACE
,
377 argv
[0], SDJOURNAL_EXTCAP_INTERFACE
,
378 argv
[0], SDJOURNAL_EXTCAP_INTERFACE
);
379 extcap_help_add_header(extcap_conf
, help_header
);
381 extcap_help_add_option(extcap_conf
, "--help", "print this help");
382 extcap_help_add_option(extcap_conf
, "--version", "print the version");
383 extcap_help_add_option(extcap_conf
, "--start-from <entry count>", "starting position");
389 extcap_help_print(extcap_conf
);
393 while ((result
= ws_getopt_long(argc
, argv
, ":", longopts
, &option_idx
)) != -1) {
398 extcap_help_print(extcap_conf
);
403 extcap_version_print(extcap_conf
);
408 start_from_entries
= (int) strtol(ws_optarg
, NULL
, 10);
409 if (errno
== EINVAL
) {
410 ws_warning("Invalid entry count: %s", ws_optarg
);
413 if (strlen(ws_optarg
) > 0 && ws_optarg
[0] == '+') {
414 start_from_end
= false;
416 if (start_from_entries
< 0) {
417 start_from_end
= true;
418 start_from_entries
*= -1;
420 ws_debug("start %d from %s", start_from_entries
, start_from_end
? "end" : "beginning");
424 /* missing option argument */
425 ws_warning("Option '%s' requires an argument", argv
[ws_optind
- 1]);
429 if (!extcap_base_parse_options(extcap_conf
, result
- EXTCAP_OPT_LIST_INTERFACES
, ws_optarg
)) {
430 ws_warning("Invalid option: %s", argv
[ws_optind
- 1]);
436 extcap_cmdline_debug(argv
, argc
);
438 if (extcap_base_handle_interface(extcap_conf
)) {
443 if (extcap_conf
->show_config
) {
444 ret
= list_config(extcap_conf
->interface
);
448 if (extcap_conf
->capture
) {
449 ret
= sdj_start_export(start_from_entries
, start_from_end
, extcap_conf
->fifo
);
451 ws_debug("You should not come here... maybe some parameter missing?");
457 extcap_base_cleanup(&extcap_conf
);
462 * Editor modelines - https://www.wireshark.org/tools/modelines.html
467 * indent-tabs-mode: t
470 * vi: set shiftwidth=8 tabstop=8 noexpandtab:
471 * :indentSize=8:tabSize=8:noTabs=false: