1 /* sdjournal.c
2  * sdjournal is an extcap tool used to dump systemd journal entries.
3  *
4  * Adapted from sshdump.
5  * Copyright 2018, Gerald Combs and Dario Lombardo
6  *
7  * Wireshark - Network traffic analyzer
8  * By Gerald Combs <gerald@wireshark.org>
9  * Copyright 1998 Gerald Combs
10  *
11  * SPDX-License-Identifier: GPL-2.0-or-later
12  */
13 
14 /*
15  * To do:
16  * - Add an option for sd_journal_open flags, e.g. SD_JOURNAL_LOCAL_ONLY.
17  * - Add journalctl options - --boot, --machine, --directory, etc.
18  */
19 
20 #include "config.h"
21 #define WS_LOG_DOMAIN "sdjournal"
22 
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>
31 
32 #include <systemd/sd-journal.h>
33 #include <systemd/sd-id128.h>
34 
35 #include <errno.h>
36 #include <string.h>
37 #include <fcntl.h>
38 
39 #define SDJOURNAL_VERSION_MAJOR "1"
40 #define SDJOURNAL_VERSION_MINOR "0"
41 #define SDJOURNAL_VERSION_RELEASE "0"
42 
43 #define SDJOURNAL_EXTCAP_INTERFACE "sdjournal"
44 #define BLOCK_TYPE_SYSTEMD_JOURNAL_EXPORT 0x00000009
45 
46 enum {
47 	EXTCAP_BASE_OPTIONS_ENUM,
48 	OPT_HELP,
49 	OPT_VERSION,
50 	OPT_START_FROM
51 };
52 
53 static struct ws_option longopts[] = {
54 	EXTCAP_BASE_OPTIONS,
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},
58 	{ 0, 0, 0, 0}
59 };
60 
61 #define FLD_BOOT_ID "_BOOT_ID="
62 #define FLD_BOOT_ID_LEN (8 + 1 + 33 + 1)
63 
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
68 
sdj_dump_entries(sd_journal * jnl,FILE * fp)69 static int sdj_dump_entries(sd_journal *jnl, FILE* fp)
70 {
71 	int ret = EXIT_SUCCESS;
72 	guint8 *entry_buff = g_new(guint8, ENTRY_BUF_LENGTH);
73 	int jr = 0;
74 
75 	/*
76 	 * Read journal entries and write them as packets. Output must
77 	 * match `journalctl --output=export`.
78 	 */
79 	while (jr == 0) {
80 		char *cursor;
81 		uint64_t pkt_rt_ts, mono_ts;
82 		sd_id128_t boot_id;
83 		char boot_id_str[FLD_BOOT_ID_LEN] = FLD_BOOT_ID;
84 		guint32 block_type = BLOCK_TYPE_SYSTEMD_JOURNAL_EXPORT;
85 		guint32 data_end = 8; // Block type + total length
86 		const void *fld_data;
87 		size_t fld_len;
88 		guint64 bytes_written = 0;
89 		int err;
90 
91 		memcpy(entry_buff, &block_type, 4);
92 
93 		jr = sd_journal_next(jnl);
94 		ws_debug("sd_journal_next: %d", jr);
95 		if (jr < 0) {
96 			ws_warning("Error fetching journal entry: %s", g_strerror(jr));
97 			goto end;
98 		} else if (jr == 0) {
99 			sd_journal_wait(jnl, (uint64_t) -1);
100 			continue;
101 		}
102 
103 		jr = sd_journal_get_cursor(jnl, &cursor);
104 		if (jr < 0) {
105 			ws_warning("Error fetching cursor: %s", g_strerror(jr));
106 			goto end;
107 		}
108 		data_end += g_snprintf(entry_buff+data_end, MAX_EXPORT_ENTRY_LENGTH-data_end, "__CURSOR=%s\n", cursor);
109 		free(cursor);
110 
111 		jr = sd_journal_get_realtime_usec(jnl, &pkt_rt_ts);
112 		if (jr < 0) {
113 			ws_warning("Error fetching realtime timestamp: %s", g_strerror(jr));
114 			goto end;
115 		}
116 		data_end += g_snprintf(entry_buff+data_end, MAX_EXPORT_ENTRY_LENGTH-data_end, "__REALTIME_TIMESTAMP=%" G_GUINT64_FORMAT "\n", pkt_rt_ts);
117 
118 		jr = sd_journal_get_monotonic_usec(jnl, &mono_ts, &boot_id);
119 		if (jr < 0) {
120 			ws_warning("Error fetching monotonic timestamp: %s", g_strerror(jr));
121 			goto end;
122 		}
123 		sd_id128_to_string(boot_id, boot_id_str + strlen(FLD_BOOT_ID));
124 		data_end += g_snprintf(entry_buff+data_end, MAX_EXPORT_ENTRY_LENGTH-data_end, "__MONOTONIC_TIMESTAMP=%" G_GUINT64_FORMAT "\n%s\n", mono_ts, boot_id_str);
125 		ws_debug("Entry header is %u bytes", data_end);
126 
127 		SD_JOURNAL_FOREACH_DATA(jnl, fld_data, fld_len) {
128 			guint8 *eq_ptr = (guint8 *) memchr(fld_data, '=', fld_len);
129 			if (!eq_ptr) {
130 				ws_warning("Invalid field.");
131 				goto end;
132 			}
133 			if (g_utf8_validate((const char *) fld_data, (gssize) 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);
138 					break;
139 				}
140 				memcpy(entry_buff+data_end, fld_data, fld_len);
141 				data_end += (guint32) fld_len;
142 				entry_buff[data_end] = '\n';
143 				data_end++;
144 			} else {
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);
148 					break;
149 				}
150 				ptrdiff_t name_len = eq_ptr - (const guint8 *) 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);
154 				data_end+= name_len;
155 				entry_buff[data_end] = '\n';
156 				data_end++;
157 				memcpy(entry_buff+data_end, &le_data_len, 8);
158 				data_end += 8;
159 				memcpy(entry_buff+data_end, (const guint8 *) fld_data + name_len + 1, fld_len - name_len);
160 				data_end += fld_len - name_len;
161 			}
162 		}
163 
164 		if (data_end % 4) {
165 			size_t pad_len = 4 - (data_end % 4);
166 			memset(entry_buff+data_end, '\0', pad_len);
167 			data_end += pad_len;
168 		}
169 
170 		guint32 total_len = data_end + 4;
171 		memcpy (entry_buff+4, &total_len, 4);
172 		memcpy (entry_buff+data_end, &total_len, 4);
173 
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));
177 			ret = EXIT_FAILURE;
178 			break;
179 		}
180 
181 		fflush(fp);
182 	}
183 
184 end:
185 	g_free(entry_buff);
186 	return ret;
187 }
188 
sdj_start_export(const int start_from_entries,const gboolean start_from_end,const char * fifo)189 static int sdj_start_export(const int start_from_entries, const gboolean start_from_end, const char* fifo)
190 {
191 	FILE* fp = stdout;
192 	guint64 bytes_written = 0;
193 	int err;
194 	sd_journal *jnl = NULL;
195 	sd_id128_t boot_id;
196 	char boot_id_str[FLD_BOOT_ID_LEN] = FLD_BOOT_ID;
197 	int ret = EXIT_FAILURE;
198 	char* err_info = NULL;
199 	char *appname;
200 	gboolean success;
201 	int jr = 0;
202 
203 	if (g_strcmp0(fifo, "-")) {
204 		/* Open or create the output file */
205 		fp = fopen(fifo, "wb");
206 		if (fp == NULL) {
207 			ws_warning("Error creating output file: %s (%s)", fifo, g_strerror(errno));
208 			return EXIT_FAILURE;
209 		}
210 	}
211 
212 
213 	appname = g_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,
216 							NULL,    /* Comment */
217 							NULL,    /* HW */
218 							NULL,    /* OS */
219 							appname,
220 							-1,      /* section_length */
221 							&bytes_written,
222 							&err);
223 	g_free(appname);
224 
225 	if (!success) {
226 		ws_warning("Can't write pcapng file header");
227 		goto cleanup;
228 	}
229 
230 	jr = sd_journal_open(&jnl, 0);
231 	if (jr < 0) {
232 		ws_warning("Error opening journal: %s", g_strerror(jr));
233 		goto cleanup;
234 	}
235 
236 	jr = sd_id128_get_boot(&boot_id);
237 	if (jr < 0) {
238 		ws_warning("Error fetching system boot ID: %s", g_strerror(jr));
239 		goto cleanup;
240 	}
241 
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));
244 	if (jr < 0) {
245 		ws_warning("Error adding match: %s", g_strerror(jr));
246 		goto cleanup;
247 	}
248 
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);
253 
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);
257 		if (jr < 0) {
258 			ws_warning("Error starting at end: %s", g_strerror(jr));
259 			goto cleanup;
260 		}
261 		jr = sd_journal_previous_skip(jnl, (uint64_t) start_from_entries + 1);
262 		if (jr < 0) {
263 			ws_warning("Error skipping backward: %s", g_strerror(jr));
264 			goto cleanup;
265 		}
266 	} else {
267 		ws_debug("Attempting to seek %d entries from the beginning", start_from_entries);
268 		jr = sd_journal_seek_head(jnl);
269 		if (jr < 0) {
270 			ws_warning("Error starting at beginning: %s", g_strerror(jr));
271 			goto cleanup;
272 		}
273 		if (start_from_entries > 0) {
274 			jr = sd_journal_next_skip(jnl, (uint64_t) start_from_entries);
275 			if (jr < 0) {
276 				ws_warning("Error skipping forward: %s", g_strerror(jr));
277 				goto cleanup;
278 			}
279 		}
280 	}
281 
282 	/* read from channel and write into fp */
283 	if (sdj_dump_entries(jnl, fp) != 0) {
284 		ws_warning("Error dumping entries");
285 		goto cleanup;
286 	}
287 
288 	ret = EXIT_SUCCESS;
289 
290 cleanup:
291 	if (jnl) {
292 		sd_journal_close(jnl);
293 	}
294 
295 	if (err_info) {
296 		ws_warning("%s", err_info);
297 	}
298 
299 	g_free(err_info);
300 
301 	/* clean up and exit */
302 	if (g_strcmp0(fifo, "-")) {
303 		fclose(fp);
304 	}
305 	return ret;
306 }
307 
list_config(char * interface)308 static int list_config(char *interface)
309 {
310 	unsigned inc = 0;
311 
312 	if (!interface) {
313 		ws_warning("ERROR: No interface specified.");
314 		return EXIT_FAILURE;
315 	}
316 
317 	if (g_strcmp0(interface, SDJOURNAL_EXTCAP_INTERFACE)) {
318 		ws_warning("ERROR: interface must be %s", SDJOURNAL_EXTCAP_INTERFACE);
319 		return EXIT_FAILURE;
320 	}
321 
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++);
326 
327 	extcap_config_debug(&inc);
328 
329 	return EXIT_SUCCESS;
330 }
331 
main(int argc,char ** argv)332 int main(int argc, char **argv)
333 {
334 	char* init_progfile_dir_error;
335 	int result;
336 	int option_idx = 0;
337 	int start_from_entries = 10;
338 	gboolean start_from_end = TRUE;
339 	int ret = EXIT_FAILURE;
340 	extcap_parameters* extcap_conf = g_new0(extcap_parameters, 1);
341 	char* help_url;
342 	char* help_header = NULL;
343 
344 	/* Initialize log handler early so we can have proper logging during startup. */
345 	extcap_log_init("sdjournal");
346 
347 	/*
348 	 * Get credential information for later use.
349 	 */
350 	init_process_policies();
351 
352 	/*
353 	 * Attempt to get the pathname of the directory containing the
354 	 * executable file.
355 	 */
356 	init_progfile_dir_error = init_progfile_dir(argv[0]);
357 	if (init_progfile_dir_error != NULL) {
358 		ws_warning("Can't get pathname of directory containing the extcap program: %s.",
359 			init_progfile_dir_error);
360 		g_free(init_progfile_dir_error);
361 	}
362 
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);
366 	g_free(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");
369 
370 	help_header = g_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",
375 			argv[0],
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);
380 	g_free(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");
384 
385 	ws_opterr = 0;
386 	ws_optind = 0;
387 
388 	if (argc == 1) {
389 		extcap_help_print(extcap_conf);
390 		goto end;
391 	}
392 
393 	while ((result = ws_getopt_long(argc, argv, ":", longopts, &option_idx)) != -1) {
394 
395 		switch (result) {
396 
397 			case OPT_HELP:
398 				extcap_help_print(extcap_conf);
399 				ret = EXIT_SUCCESS;
400 				goto end;
401 
402 			case OPT_VERSION:
403 				extcap_version_print(extcap_conf);
404 				ret = EXIT_SUCCESS;
405 				goto end;
406 
407 			case OPT_START_FROM:
408 				start_from_entries = (int) strtol(ws_optarg, NULL, 10);
409 				if (errno == EINVAL) {
410 					ws_warning("Invalid entry count: %s", ws_optarg);
411 					goto end;
412 				}
413 				if (strlen(ws_optarg) > 0 && ws_optarg[0] == '+') {
414 					start_from_end = FALSE;
415 				}
416 				if (start_from_entries < 0) {
417 					start_from_end = TRUE;
418 					start_from_entries *= -1;
419 				}
420 				ws_debug("start %d from %s", start_from_entries, start_from_end ? "end" : "beginning");
421 				break;
422 
423 			case ':':
424 				/* missing option argument */
425 				ws_warning("Option '%s' requires an argument", argv[ws_optind - 1]);
426 				break;
427 
428 			default:
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]);
431 					goto end;
432 				}
433 		}
434 	}
435 
436 	extcap_cmdline_debug(argv, argc);
437 
438 	if (extcap_base_handle_interface(extcap_conf)) {
439 		ret = EXIT_SUCCESS;
440 		goto end;
441 	}
442 
443 	if (extcap_conf->show_config) {
444 		ret = list_config(extcap_conf->interface);
445 		goto end;
446 	}
447 
448 	if (extcap_conf->capture) {
449 		ret = sdj_start_export(start_from_entries, start_from_end, extcap_conf->fifo);
450 	} else {
451 		ws_debug("You should not come here... maybe some parameter missing?");
452 		ret = EXIT_FAILURE;
453 	}
454 
455 end:
456 	/* clean up stuff */
457 	extcap_base_cleanup(&extcap_conf);
458 	return ret;
459 }
460 
461 /*
462  * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
463  *
464  * Local variables:
465  * c-basic-offset: 8
466  * tab-width: 8
467  * indent-tabs-mode: t
468  * End:
469  *
470  * vi: set shiftwidth=8 tabstop=8 noexpandtab:
471  * :indentSize=8:tabSize=8:noTabs=false:
472  */
473