diff --git a/NEWS b/NEWS
index bc7ee1e2bf7..4e2166feffb 100644
--- a/NEWS
+++ b/NEWS
@@ -78,6 +78,12 @@ CHANGES WITH 261 in spe:
configuration extensions merged from the main system itself cannot be
used to modify the resources which are used in the early boot.
+ Changes in systemd-networkd and networkctl:
+
+ * A new 'networkctl dhcp-lease INTERFACE' command has been added to
+ dump acquired DHCP leases. This may be useful for inspecting the
+ DHCP options provided by the server.
+
CHANGES WITH 260:
Feature Removals and Incompatible Changes:
diff --git a/man/networkctl.xml b/man/networkctl.xml
index 561eb932386..b436797999e 100644
--- a/man/networkctl.xml
+++ b/man/networkctl.xml
@@ -287,6 +287,122 @@
+
+
+ dhcp-lease
+ INTERFACE
+ CODE:FORMAT
+ …
+
+
+
+ Show the DHCP message of the acquired lease (i.e. the received DHCPACK message). Takes an
+ interface name and optional DHCP option codes. If no option code is specified, both the DHCP
+ message header and all options are shown. If one or more option codes are specified, only the
+ specified options are shown.
+
+ When called with the option, the entire DHCP message is shown in
+ JSON format. In that case, any specified option codes are ignored.
+
+ By default, each option is displayed in a human-readable format, depending on the option.
+ For example, DNS server addresses are shown as a list of IP addresses (e.g.
+ 192.0.2.1), and the lease time is shown as a human-readable duration (e.g.
+ 8h).
+
+ Each option code may optionally be followed by a formatter, separated by a colon (e.g.
+ 42:address). This can be useful if the default formatting is not appropriate.
+
+
+ The following formatters are supported:
+
+
+
+
+ Use the default formatting.
+
+
+
+
+
+ Show the raw bytes in hexadecimal, separated by colons.
+
+
+
+
+
+ Treat the option as having zero-length data and display yes.
+
+
+
+
+
+ Treat the option as a 1-byte boolean and display yes or no.
+
+
+
+
+
+ Treat the option as a 1-byte unsigned integer.
+
+
+
+
+
+ Treat the option as a 2-byte unsigned integer.
+
+
+
+
+
+ Treat the option as a 4-byte time value in seconds and display it as a human-readable duration.
+
+
+
+
+
+ Treat the option as a string.
+
+
+
+
+
+ Treat the option as one or more IPv4 addresses and display them in human-readable form.
+
+
+
+
+ Produces output similar to:
+ $ networkctl dhcp-lease eth0
+Header:
+ KEY VALUE
+ Hardware Type: ETHER
+Hardware Address: 41:42:43:31:32:33
+ Client Address: 192.0.2.123
+ Server Address: 192.0.2.1
+
+Options:
+CODE NAME DATA
+ 1 subnet mask 255.255.255.0
+ 3 router 192.0.2.1
+ 6 domain name server 192.0.2.1
+ 12 hostname test-node
+ 15 domain name lan
+ 28 broadcast address 192.0.2.255
+ 42 NTP server 192.0.2.11
+ 192.0.2.12
+ 51 lease time 1d
+ 53 message type 5
+ 54 server identifier 192.0.2.1
+ 58 renewal time 11h 5min 38s
+ 59 rebinding time 20h 5min 38s
+ 119 domain search hoge.example.com
+ foo.example.com
+
+
+
+
+
+
lldp
diff --git a/shell-completion/bash/networkctl b/shell-completion/bash/networkctl
index c16768079e3..fe186a49a2e 100644
--- a/shell-completion/bash/networkctl
+++ b/shell-completion/bash/networkctl
@@ -51,7 +51,7 @@ _networkctl() {
local -A VERBS=(
[STANDALONE]='label reload'
- [LINKS]='status list lldp delete renew up down forcerenew reconfigure'
+ [LINKS]='status dhcp-lease list lldp delete renew up down forcerenew reconfigure'
[FILES_OR_LINKS]='edit cat'
[FILES]='mask unmask'
[BOOL]='persistent-storage'
diff --git a/shell-completion/zsh/_networkctl b/shell-completion/zsh/_networkctl
index cf072c0fcbb..c44b3469498 100644
--- a/shell-completion/zsh/_networkctl
+++ b/shell-completion/zsh/_networkctl
@@ -7,6 +7,7 @@
_networkctl_cmds=(
'list:List existing links'
'status:Show information about the specified links'
+ 'dhcp-lease:Show DHCP lease'
'lldp:Show Link Layer Discovery Protocol status'
'label:Show address labels'
'delete:Delete virtual netdevs'
@@ -26,7 +27,7 @@
local -a _links
cmd="${${_networkctl_cmds[(r)$words[1]:*]%%:*}}"
case $cmd in
- (list|status|up|down|cat|edit|lldp|delete|renew|forcerenew|reconfigure)
+ (list|status|dhcp-lease|up|down|cat|edit|lldp|delete|renew|forcerenew|reconfigure)
for link in ${(f)"$(_call_program links networkctl list --no-legend)"}; do _links+=($link[(w)2]:$link); done
if [[ -n "$_links" ]]; then
_describe -t links 'links' _links $( [[ $cmd == (edit|cat) ]] && print -- -P@ )
diff --git a/src/libsystemd-network/dhcp-message-dump.c b/src/libsystemd-network/dhcp-message-dump.c
new file mode 100644
index 00000000000..3c9283351a3
--- /dev/null
+++ b/src/libsystemd-network/dhcp-message-dump.c
@@ -0,0 +1,1178 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "ansi-color.h"
+#include "arphrd-util.h"
+#include "dhcp-message-dump.h"
+#include "dhcp-protocol.h"
+#include "dhcp-route.h"
+#include "dns-resolver-internal.h"
+#include "escape.h"
+#include "ether-addr-util.h"
+#include "format-table.h"
+#include "hashmap.h"
+#include "hexdecoct.h"
+#include "iovec-util.h"
+#include "iovec-wrapper.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+static int iovec_to_hex(const struct iovec *iov, char **ret) {
+ assert(iov);
+ assert(ret);
+
+ _cleanup_free_ char *str = new(char, iov->iov_len * 3);
+ if (!str)
+ return log_oom();
+
+ char *p = str;
+ FOREACH_ARRAY(v, ((uint8_t*) iov->iov_base), iov->iov_len) {
+ if (p != str)
+ *p++ = ':';
+ *p++ = hexchar(*v >> 4);
+ *p++ = hexchar(*v & 0x0f);
+ }
+ *p = '\0';
+
+ *ret = TAKE_PTR(str);
+ return 0;
+}
+
+static int iovw_to_strv(const struct iovec_wrapper *iovw, char ***ret) {
+ int r;
+
+ assert(iovw);
+ assert(ret);
+
+ _cleanup_strv_free_ char **strv = NULL;
+ size_t n_strv = 0;
+ FOREACH_ARRAY(iov, iovw->iovec, iovw->count) {
+ _cleanup_free_ char *escaped = cescape_length(iov->iov_base, iov->iov_len);
+ if (!escaped)
+ return log_oom();
+
+ r = strv_consume_with_size(&strv, &n_strv, TAKE_PTR(escaped));
+ if (r < 0)
+ return log_oom();
+ }
+
+ *ret = TAKE_PTR(strv);
+ return 0;
+}
+
+static void table_apply_flags(Table *table, DumpDHCPMessageFlag flags) {
+ assert(table);
+
+ table_set_header(table, FLAGS_SET(flags, DUMP_DHCP_MESSAGE_LEGEND));
+ if (FLAGS_SET(flags, DUMP_DHCP_MESSAGE_FULL))
+ table_set_width(table, 0);
+}
+
+static int dump_dhcp_option_vendor_specific_information(sd_dhcp_message *m, DumpDHCPMessageFlag flags) {
+ int r;
+
+ assert(m);
+
+ _cleanup_(table_unrefp) Table *table = table_new("code", "data");
+ if (!table)
+ return log_oom();
+
+ table_apply_flags(table, flags);
+
+ (void) table_set_sort(table, (size_t) 0);
+
+ TableCell *cell = table_get_cell(table, 0, 0);
+ if (!cell)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get table cell.");
+
+ (void) table_set_align_percent(table, cell, 100);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ _cleanup_(tlv_unrefp) TLV *tlv = NULL;
+ r = dhcp_message_get_option_sub_tlv(m, SD_DHCP_OPTION_VENDOR_SPECIFIC_INFORMATION, TLV_DHCP4_SUBOPTION, &tlv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read DHCP option %i: %m", SD_DHCP_OPTION_VENDOR_SPECIFIC_INFORMATION);
+
+ void *tagp;
+ struct iovec_wrapper *iovw;
+ HASHMAP_FOREACH_KEY(iovw, tagp, tlv->entries)
+ FOREACH_ARRAY(iov, iovw->iovec, iovw->count) {
+ _cleanup_free_ char *str = NULL;
+ r = iovec_to_hex(iov, &str);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(
+ table,
+ TABLE_UINT32, PTR_TO_UINT32(tagp),
+ TABLE_STRING, str);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ putchar('\n');
+ if (FLAGS_SET(flags, DUMP_DHCP_MESSAGE_LEGEND))
+ printf("%s%sVendor-Specific Information:%s\n", ansi_highlight(), ansi_add_underline(), ansi_normal());
+
+ return table_print_or_warn(table);
+}
+
+static int dump_dhcp_option_vendor_identifying_vendor_class(sd_dhcp_message *m, DumpDHCPMessageFlag flags) {
+ int r;
+
+ assert(m);
+
+ _cleanup_(table_unrefp) Table *table = table_new("enterprise-number", "data");
+ if (!table)
+ return log_oom();
+
+ table_apply_flags(table, flags);
+
+ (void) table_set_sort(table, (size_t) 0);
+
+ TableCell *cell = table_get_cell(table, 0, 0);
+ if (!cell)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get table cell.");
+
+ (void) table_set_align_percent(table, cell, 100);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ _cleanup_(tlv_unrefp) TLV *tlv = NULL;
+ r = dhcp_message_get_option_sub_tlv(m, SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_CLASS, TLV_DHCP4_VENDOR_IDENTIFYING_OPTION, &tlv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read DHCP option %i: %m", SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_CLASS);
+
+ void *key, *value;
+ HASHMAP_FOREACH_KEY(value, key, tlv->entries) {
+ uint32_t enterprise_number = PTR_TO_UINT32(key);
+
+ _cleanup_(iovec_done) struct iovec iov = {};
+ r = tlv_get_alloc(tlv, enterprise_number, &iov);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read vendor class of enterprise number %"PRIu32": %m", enterprise_number);
+
+ _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
+ r = iovec_split(&iov, /* length_size= */ 1, &iovw);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse vendor class of enterprise number %"PRIu32": %m", enterprise_number);
+
+ _cleanup_strv_free_ char **strv = NULL;
+ r = iovw_to_strv(&iovw, &strv);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(
+ table,
+ TABLE_UINT32, enterprise_number,
+ TABLE_STRV, strv);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ putchar('\n');
+ if (FLAGS_SET(flags, DUMP_DHCP_MESSAGE_LEGEND))
+ printf("%s%sVendor-Identifying Vendor Class:%s\n", ansi_highlight(), ansi_add_underline(), ansi_normal());
+
+ return table_print_or_warn(table);
+}
+
+static int dump_dhcp_option_vendor_identifying_vendor_specific_information(sd_dhcp_message *m, DumpDHCPMessageFlag flags) {
+ int r;
+
+ assert(m);
+
+ _cleanup_(table_unrefp) Table *table = table_new("enterprise-number", "code", "data");
+ if (!table)
+ return log_oom();
+
+ table_apply_flags(table, flags);
+
+ (void) table_set_sort(table, (size_t) 0, (size_t) 1);
+
+ for (unsigned i = 0; i <= 1; i++) {
+ TableCell *cell = table_get_cell(table, 0, i);
+ if (!cell)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get table cell.");
+
+ (void) table_set_align_percent(table, cell, 100);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+ }
+
+ _cleanup_(tlv_unrefp) TLV *tlv = NULL;
+ r = dhcp_message_get_option_sub_tlv(m, SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_SPECIFIC_INFORMATION, TLV_DHCP4_VENDOR_IDENTIFYING_OPTION, &tlv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read DHCP option %i: %m", SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_SPECIFIC_INFORMATION);
+
+ void *key, *value;
+ HASHMAP_FOREACH_KEY(value, key, tlv->entries) {
+ uint32_t enterprise_number = PTR_TO_UINT32(key);
+
+ _cleanup_(iovec_done) struct iovec iov = {};
+ r = tlv_get_alloc(tlv, enterprise_number, &iov);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read vendor specific information of enterprise number %"PRIu32": %m", enterprise_number);
+
+ _cleanup_(tlv_done) TLV sub_tlv = TLV_INIT(TLV_DHCP4_SUBOPTION);
+ r = tlv_parse(&sub_tlv, &iov);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse vendor specific information of enterprise number %"PRIu32": %m", enterprise_number);
+
+ void *tagp;
+ struct iovec_wrapper *iovw;
+ HASHMAP_FOREACH_KEY(iovw, tagp, sub_tlv.entries) {
+ uint32_t code = PTR_TO_UINT32(tagp);
+
+ FOREACH_ARRAY(i, iovw->iovec, iovw->count) {
+ _cleanup_free_ char *str = NULL;
+ r = iovec_to_hex(i, &str);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(
+ table,
+ TABLE_UINT32, enterprise_number,
+ TABLE_UINT32, code,
+ TABLE_STRING, str);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+ }
+
+ putchar('\n');
+ if (FLAGS_SET(flags, DUMP_DHCP_MESSAGE_LEGEND))
+ printf("%s%sVendor-Identifying Vendor-Specific Information:%s\n", ansi_highlight(), ansi_add_underline(), ansi_normal());
+
+ return table_print_or_warn(table);
+}
+
+typedef enum DHCPOptionType {
+ DHCP_OPTION_TYPE_AUTO,
+ DHCP_OPTION_TYPE_HEX,
+ DHCP_OPTION_TYPE_FLAG,
+ DHCP_OPTION_TYPE_BOOL,
+ DHCP_OPTION_TYPE_UINT8,
+ DHCP_OPTION_TYPE_UINT16,
+ DHCP_OPTION_TYPE_TIME,
+ DHCP_OPTION_TYPE_STRING,
+ DHCP_OPTION_TYPE_ADDRESS,
+ _DHCP_OPTION_TYPE_AUTO_MAX,
+ DHCP_OPTION_TYPE_SIP = _DHCP_OPTION_TYPE_AUTO_MAX,
+ DHCP_OPTION_TYPE_FQDN,
+ DHCP_OPTION_TYPE_ROUTE,
+ DHCP_OPTION_TYPE_LENGTH_PREFIXED_DATA,
+ DHCP_OPTION_TYPE_SEARCH_DOMAINS,
+ DHCP_OPTION_TYPE_DNR,
+ DHCP_OPTION_TYPE_6RD,
+ DHCP_OPTION_TYPE_TBD,
+ _DHCP_OPTION_TYPE_MAX,
+ _DHCP_OPTION_TYPE_INVALID = -EINVAL,
+} DHCPOptionType;
+
+static const char * const dhcp_option_type_table[_DHCP_OPTION_TYPE_AUTO_MAX] = {
+ [DHCP_OPTION_TYPE_AUTO] = "auto",
+ [DHCP_OPTION_TYPE_HEX] = "hex",
+ [DHCP_OPTION_TYPE_FLAG] = "flag",
+ [DHCP_OPTION_TYPE_BOOL] = "bool",
+ [DHCP_OPTION_TYPE_UINT8] = "uint8",
+ [DHCP_OPTION_TYPE_UINT16] = "uint16",
+ [DHCP_OPTION_TYPE_TIME] = "time",
+ [DHCP_OPTION_TYPE_STRING] = "string",
+ [DHCP_OPTION_TYPE_ADDRESS] = "address",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(dhcp_option_type, DHCPOptionType);
+
+static DHCPOptionType dhcp_option_type_from_code(uint8_t code) {
+ switch (code) {
+ case SD_DHCP_OPTION_PAD:
+ return -EINVAL;
+ case SD_DHCP_OPTION_SUBNET_MASK:
+ case SD_DHCP_OPTION_ROUTER:
+ case SD_DHCP_OPTION_TIME_SERVER:
+ case SD_DHCP_OPTION_NAME_SERVER:
+ case SD_DHCP_OPTION_DOMAIN_NAME_SERVER:
+ case SD_DHCP_OPTION_LOG_SERVER:
+ case SD_DHCP_OPTION_QUOTES_SERVER:
+ case SD_DHCP_OPTION_LPR_SERVER:
+ case SD_DHCP_OPTION_IMPRESS_SERVER:
+ case SD_DHCP_OPTION_RLP_SERVER:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_HOST_NAME:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_BOOT_FILE_SIZE:
+ return DHCP_OPTION_TYPE_UINT16;
+ case SD_DHCP_OPTION_MERIT_DUMP_FILE:
+ case SD_DHCP_OPTION_DOMAIN_NAME:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_SWAP_SERVER:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_ROOT_PATH:
+ case SD_DHCP_OPTION_EXTENSION_FILE:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_FORWARD:
+ case SD_DHCP_OPTION_SOURCE_ROUTE:
+ return DHCP_OPTION_TYPE_BOOL;
+ case SD_DHCP_OPTION_MAX_DATAGRAM_ASSEMBLY:
+ return DHCP_OPTION_TYPE_UINT16;
+ case SD_DHCP_OPTION_DEFAULT_IP_TTL:
+ return DHCP_OPTION_TYPE_UINT8;
+ case SD_DHCP_OPTION_MTU_TIMEOUT:
+ return DHCP_OPTION_TYPE_TIME;
+ case SD_DHCP_OPTION_MTU_INTERFACE:
+ return DHCP_OPTION_TYPE_UINT16;
+ case SD_DHCP_OPTION_MTU_SUBNET:
+ return DHCP_OPTION_TYPE_BOOL;
+ case SD_DHCP_OPTION_BROADCAST:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_MASK_DISCOVERY:
+ case SD_DHCP_OPTION_MASK_SUPPLIER:
+ case SD_DHCP_OPTION_ROUTER_DISCOVERY:
+ return DHCP_OPTION_TYPE_BOOL;
+ case SD_DHCP_OPTION_ROUTER_REQUEST:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_STATIC_ROUTE:
+ return DHCP_OPTION_TYPE_ROUTE;
+ case SD_DHCP_OPTION_TRAILERS:
+ return DHCP_OPTION_TYPE_BOOL;
+ case SD_DHCP_OPTION_ARP_TIMEOUT:
+ return DHCP_OPTION_TYPE_TIME;
+ case SD_DHCP_OPTION_ETHERNET:
+ return DHCP_OPTION_TYPE_BOOL;
+ case SD_DHCP_OPTION_DEFAULT_TCP_TTL:
+ return DHCP_OPTION_TYPE_UINT8;
+ case SD_DHCP_OPTION_KEEPALIVE_TIME:
+ return DHCP_OPTION_TYPE_TIME;
+ case SD_DHCP_OPTION_KEEPALIVE_DATA:
+ return DHCP_OPTION_TYPE_BOOL;
+ case SD_DHCP_OPTION_NIS_DOMAIN:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_NIS_SERVER:
+ case SD_DHCP_OPTION_NTP_SERVER:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_VENDOR_SPECIFIC_INFORMATION:
+ return DHCP_OPTION_TYPE_TBD;
+ case SD_DHCP_OPTION_NETBIOS_NAME_SERVER:
+ case SD_DHCP_OPTION_NETBIOS_DIST_SERVER:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_NETBIOS_NODE_TYPE:
+ return DHCP_OPTION_TYPE_UINT8;
+ case SD_DHCP_OPTION_NETBIOS_SCOPE:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_X_WINDOW_FONT:
+ case SD_DHCP_OPTION_X_WINDOW_MANAGER:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_REQUESTED_IP_ADDRESS:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME:
+ return DHCP_OPTION_TYPE_TIME;
+ case SD_DHCP_OPTION_OVERLOAD:
+ case SD_DHCP_OPTION_MESSAGE_TYPE:
+ return DHCP_OPTION_TYPE_UINT8;
+ case SD_DHCP_OPTION_SERVER_IDENTIFIER:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_ERROR_MESSAGE:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_MAXIMUM_MESSAGE_SIZE:
+ return DHCP_OPTION_TYPE_UINT16;
+ case SD_DHCP_OPTION_RENEWAL_TIME:
+ case SD_DHCP_OPTION_REBINDING_TIME:
+ return DHCP_OPTION_TYPE_TIME;
+ case SD_DHCP_OPTION_VENDOR_CLASS_IDENTIFIER:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_NETWARE_IP_DOMAIN:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_NIS_DOMAIN_NAME:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_NIS_SERVER_ADDR:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_BOOT_SERVER_NAME:
+ case SD_DHCP_OPTION_BOOT_FILENAME:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_HOME_AGENT_ADDRESS:
+ case SD_DHCP_OPTION_SMTP_SERVER:
+ case SD_DHCP_OPTION_POP3_SERVER:
+ case SD_DHCP_OPTION_NNTP_SERVER:
+ case SD_DHCP_OPTION_WWW_SERVER:
+ case SD_DHCP_OPTION_FINGER_SERVER:
+ case SD_DHCP_OPTION_IRC_SERVER:
+ case SD_DHCP_OPTION_STREETTALK_SERVER:
+ case SD_DHCP_OPTION_STDA_SERVER:
+ return DHCP_OPTION_TYPE_ADDRESS;
+ case SD_DHCP_OPTION_USER_CLASS:
+ return DHCP_OPTION_TYPE_LENGTH_PREFIXED_DATA;
+ case SD_DHCP_OPTION_RAPID_COMMIT:
+ return DHCP_OPTION_TYPE_FLAG;
+ case SD_DHCP_OPTION_FQDN:
+ return DHCP_OPTION_TYPE_FQDN;
+ case SD_DHCP_OPTION_POSIX_TIMEZONE:
+ case SD_DHCP_OPTION_TZDB_TIMEZONE:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_IPV6_ONLY_PREFERRED:
+ return DHCP_OPTION_TYPE_TIME;
+ case SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_DOMAIN_SEARCH:
+ return DHCP_OPTION_TYPE_SEARCH_DOMAINS;
+ case SD_DHCP_OPTION_SIP_SERVER:
+ return DHCP_OPTION_TYPE_SIP;
+ case SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE:
+ return DHCP_OPTION_TYPE_ROUTE;
+ case SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_CLASS:
+ case SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_SPECIFIC_INFORMATION:
+ return DHCP_OPTION_TYPE_TBD;
+ case SD_DHCP_OPTION_MUD_URL:
+ return DHCP_OPTION_TYPE_STRING;
+ case SD_DHCP_OPTION_V4_DNR:
+ return DHCP_OPTION_TYPE_DNR;
+ case SD_DHCP_OPTION_6RD:
+ return DHCP_OPTION_TYPE_6RD;
+ case SD_DHCP_OPTION_PRIVATE_CLASSLESS_STATIC_ROUTE:
+ return DHCP_OPTION_TYPE_ROUTE;
+ case SD_DHCP_OPTION_END:
+ return -EINVAL;
+ default:
+ return DHCP_OPTION_TYPE_HEX;
+ }
+}
+
+static int dump_dhcp_option_hex(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ _cleanup_(iovec_done) struct iovec iov = {};
+ r = dhcp_message_get_option_alloc(message, code, &iov);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read DHCP option %u: %m", code);
+
+ _cleanup_free_ char *str = NULL;
+ r = iovec_to_hex(&iov, &str);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRING, str);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_flag(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ r = dhcp_message_get_option_flag(message, code);
+ if (r < 0) {
+ if (fallback)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+ return log_error_errno(r, "Failed to dump DHCP option %u as flag: %m", code);
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_BOOLEAN, true);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_bool(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ uint8_t u;
+ r = dhcp_message_get_option_u8(message, code, &u);
+ if (r < 0) {
+ if (fallback)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+ return log_error_errno(r, "Failed to read option %u as boolean: %m", code);
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_BOOLEAN, !!u);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_uint8(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ uint8_t u;
+ r = dhcp_message_get_option_u8(message, code, &u);
+ if (r < 0) {
+ if (fallback)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+ return log_error_errno(r, "Failed to read option %u as uint8: %m", code);
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_UINT8, u);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_uint16(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ uint16_t u;
+ r = dhcp_message_get_option_u16(message, code, &u);
+ if (r < 0) {
+ if (fallback)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+ return log_error_errno(r, "Failed to read option %u as uint16: %m", code);
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_UINT16, u);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_time(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ usec_t usec;
+ r = dhcp_message_get_option_sec(message, code, /* max_as_infinity= */ true, &usec);
+ if (r < 0) {
+ if (fallback)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+ return log_error_errno(r, "Failed to read option %u as time: %m", code);
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_TIMESPAN, usec);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_string(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ _cleanup_free_ char *str = NULL;
+ r = dhcp_message_get_option_string(message, code, &str);
+ if (r < 0) {
+ if (fallback)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+ return log_error_errno(r, "Failed to read option %u as string: %m", code);
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRING, str);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_address(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ _cleanup_free_ struct in_addr *addrs = NULL;
+ size_t n_addrs;
+ r = dhcp_message_get_option_addresses(message, code, &n_addrs, &addrs);
+ if (r < 0) {
+ if (fallback)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+ return log_error_errno(r, "Failed to read option %u as address: %m", code);
+ }
+
+ _cleanup_strv_free_ char **strv = NULL;
+ size_t n_strv = 0;
+ FOREACH_ARRAY(a, addrs, n_addrs) {
+ r = strv_extend_with_size(&strv, &n_strv, IN4_ADDR_TO_STRING(a));
+ if (r < 0)
+ return log_oom();
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRV, strv);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_sip(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+ assert(code == SD_DHCP_OPTION_SIP_SERVER);
+
+ _cleanup_strv_free_ char **strv = NULL;
+ if (dhcp_message_get_option_domains(message, code, &strv) < 0)
+ return dump_dhcp_option_address(table, message, code, /* fallback= */ true);
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRV, strv);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_fqdn(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+ assert(code == SD_DHCP_OPTION_FQDN);
+
+ _cleanup_free_ char *fqdn = NULL;
+ uint8_t flags;
+ if (dhcp_message_get_option_fqdn(message, &flags, &fqdn) < 0)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code));
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_cell_stringf(table, /* ret_cell= */ NULL, "flags: 0x%x, fqdn: %s", flags, fqdn);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_route(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ _cleanup_free_ sd_dhcp_route *routes = NULL;
+ size_t n_routes;
+ if (dhcp_message_get_option_routes(message, code, &n_routes, &routes) < 0)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+
+ _cleanup_strv_free_ char **strv = NULL;
+ size_t n_strv = 0;
+ FOREACH_ARRAY(route, routes, n_routes) {
+ r = strv_extendf_with_size(&strv, &n_strv, "%s via %s",
+ IN4_ADDR_PREFIX_TO_STRING(&route->dst_addr, route->dst_prefixlen),
+ IN4_ADDR_TO_STRING(&route->gw_addr));
+ if (r < 0)
+ return log_oom();
+ }
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRV, strv);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_length_prefixed_data(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
+ if (dhcp_message_get_option_length_prefixed_data(message, code, /* length_size= */ 1, &iovw) < 0)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+
+ _cleanup_strv_free_ char **strv = NULL;
+ r = iovw_to_strv(&iovw, &strv);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRV, strv);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_tbd(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+
+ if (!dhcp_message_has_option(message, code))
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "The DHCP message does not have option %u.", code);
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRING, "See below.");
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_search_domains(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+ assert(code == SD_DHCP_OPTION_DOMAIN_SEARCH);
+
+ _cleanup_strv_free_ char **strv = NULL;
+ if (dhcp_message_get_option_domains(message, code, &strv) < 0)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRV, strv);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_dnr(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+ assert(code == SD_DHCP_OPTION_V4_DNR);
+
+ sd_dns_resolver *resolvers = NULL;
+ size_t n_resolvers = 0;
+ CLEANUP_ARRAY(resolvers, n_resolvers, dns_resolver_free_array);
+
+ if (dhcp_message_get_option_dnr(message, &n_resolvers, &resolvers) < 0)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+
+ _cleanup_strv_free_ char **strv = NULL;
+ r = dns_resolvers_to_dot_strv(resolvers, n_resolvers, &strv);
+ if (r < 0)
+ return log_oom();
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code),
+ TABLE_STRV, strv);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_6rd(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback) {
+ int r;
+
+ assert(table);
+ assert(message);
+ assert(code == SD_DHCP_OPTION_6RD);
+
+ uint8_t ipv4masklen, prefixlen;
+ struct in6_addr prefix;
+ size_t n_br_addresses;
+ _cleanup_free_ struct in_addr *br_addresses = NULL;
+ if (dhcp_message_get_option_6rd(message, &ipv4masklen, &prefixlen, &prefix, &n_br_addresses, &br_addresses) < 0)
+ return dump_dhcp_option_hex(table, message, code, /* fallback= */ false);
+
+ _cleanup_free_ char *str = asprintf_safe("ipv4masklen: %u, prefix: %s, br_addresses: ",
+ ipv4masklen, IN6_ADDR_PREFIX_TO_STRING(&prefix, prefixlen));
+ if (!str)
+ return log_oom();
+
+ assert(n_br_addresses > 0);
+ _cleanup_free_ char *br_addresses_str = NULL;
+ FOREACH_ARRAY(a, br_addresses, n_br_addresses)
+ if (!strextend_with_separator(&br_addresses_str, ", ", IN4_ADDR_TO_STRING(a)))
+ return log_oom();
+
+ r = table_add_many(
+ table,
+ TABLE_UINT8, code,
+ TABLE_STRING, dhcp_option_code_to_string(code));
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_cell_stringf(table, /* ret_cell= */ NULL, "%s%s", str, br_addresses_str);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_option_one(Table *table, sd_dhcp_message *message, uint8_t code, DHCPOptionType type) {
+ assert(table);
+ assert(message);
+ assert(!IN_SET(code, SD_DHCP_OPTION_PAD, SD_DHCP_OPTION_END));
+
+ typedef int (*dump_dhcp_option_t)(Table *table, sd_dhcp_message *message, uint8_t code, bool fallback);
+
+ static const dump_dhcp_option_t functions[_DHCP_OPTION_TYPE_MAX] = {
+ [DHCP_OPTION_TYPE_HEX] = dump_dhcp_option_hex,
+ [DHCP_OPTION_TYPE_FLAG] = dump_dhcp_option_flag,
+ [DHCP_OPTION_TYPE_BOOL] = dump_dhcp_option_bool,
+ [DHCP_OPTION_TYPE_UINT8] = dump_dhcp_option_uint8,
+ [DHCP_OPTION_TYPE_UINT16] = dump_dhcp_option_uint16,
+ [DHCP_OPTION_TYPE_TIME] = dump_dhcp_option_time,
+ [DHCP_OPTION_TYPE_STRING] = dump_dhcp_option_string,
+ [DHCP_OPTION_TYPE_ADDRESS] = dump_dhcp_option_address,
+ [DHCP_OPTION_TYPE_SIP] = dump_dhcp_option_sip,
+ [DHCP_OPTION_TYPE_FQDN] = dump_dhcp_option_fqdn,
+ [DHCP_OPTION_TYPE_ROUTE] = dump_dhcp_option_route,
+ [DHCP_OPTION_TYPE_LENGTH_PREFIXED_DATA] = dump_dhcp_option_length_prefixed_data,
+ [DHCP_OPTION_TYPE_SEARCH_DOMAINS] = dump_dhcp_option_search_domains,
+ [DHCP_OPTION_TYPE_DNR] = dump_dhcp_option_dnr,
+ [DHCP_OPTION_TYPE_6RD] = dump_dhcp_option_6rd,
+ [DHCP_OPTION_TYPE_TBD] = dump_dhcp_option_tbd,
+ };
+
+ bool fallback = false;
+ if (type == DHCP_OPTION_TYPE_AUTO) {
+ type = dhcp_option_type_from_code(code);
+ fallback = true;
+ }
+
+ assert(functions[type]);
+ return functions[type](table, message, code, fallback);
+}
+
+static int parse_arg(const char *arg, uint8_t *ret_code, DHCPOptionType *ret_type) {
+ _cleanup_free_ char *buf = NULL;
+ const char *code_str, *type_str;
+ int r;
+
+ assert(arg);
+
+ const char *colon = strchr(arg, ':');
+ if (colon) {
+ buf = strndup(arg, colon - arg);
+ if (!buf)
+ return log_oom();
+
+ code_str = buf;
+ type_str = colon + 1;
+ } else {
+ code_str = arg;
+ type_str = NULL;
+ }
+
+ uint8_t code;
+ r = safe_atou8(code_str, &code);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse option code number '%s': %m", code_str);
+
+ if (IN_SET(code, SD_DHCP_OPTION_PAD, SD_DHCP_OPTION_END))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid option code number: %u", code);
+
+ DHCPOptionType type = DHCP_OPTION_TYPE_AUTO;
+ if (type_str) {
+ type = dhcp_option_type_from_string(type_str);
+ if (type < 0)
+ return log_error_errno(type, "Failed to parse option type '%s': %m", type_str);
+ }
+
+ if (ret_code)
+ *ret_code = code;
+ if (ret_type)
+ *ret_type = type;
+ return 0;
+}
+
+static int dump_dhcp_options(sd_dhcp_message *message, char * const *args, DumpDHCPMessageFlag flags) {
+ int r;
+
+ assert(message);
+
+ _cleanup_(table_unrefp) Table *table = table_new("code", "name", "data");
+ if (!table)
+ return log_oom();
+
+ table_apply_flags(table, flags);
+
+ (void) table_set_sort(table, (size_t) 0);
+
+ TableCell *cell = table_get_cell(table, 0, 0);
+ if (!cell)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get table cell.");
+
+ (void) table_set_align_percent(table, cell, 100);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ bool
+ has_vendor_specific_information = false,
+ has_vendor_identifying_vendor_class = false,
+ has_vendor_identifying_vendor_specific_information = false;
+
+ if (strv_isempty(args)) {
+ void *tagp;
+ struct iovec_wrapper *iovw;
+ HASHMAP_FOREACH_KEY(iovw, tagp, message->options.entries) {
+ uint32_t tag = PTR_TO_UINT32(tagp);
+ assert(tag > 0);
+ assert(tag < UINT8_MAX);
+
+ r = dump_dhcp_option_one(table, message, tag, DHCP_OPTION_TYPE_AUTO);
+ if (r < 0)
+ return r;
+
+ switch (tag) {
+ case SD_DHCP_OPTION_VENDOR_SPECIFIC_INFORMATION:
+ has_vendor_specific_information = true;
+ break;
+ case SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_CLASS:
+ has_vendor_identifying_vendor_class = true;
+ break;
+ case SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_SPECIFIC_INFORMATION:
+ has_vendor_identifying_vendor_specific_information = true;
+ break;
+ }
+ }
+ } else
+ STRV_FOREACH(arg, args) {
+ uint8_t code = SD_DHCP_OPTION_PAD; /* avoid false maybe-uninitialized warning */
+ DHCPOptionType type = DHCP_OPTION_TYPE_AUTO; /* avoid false maybe-uninitialized warning */
+ r = parse_arg(*arg, &code, &type);
+ if (r < 0)
+ return r;
+ assert(!IN_SET(code, SD_DHCP_OPTION_PAD, SD_DHCP_OPTION_END));
+
+ r = dump_dhcp_option_one(table, message, code, type);
+ if (r < 0)
+ return r;
+
+ if (type == DHCP_OPTION_TYPE_AUTO)
+ switch (code) {
+ case SD_DHCP_OPTION_VENDOR_SPECIFIC_INFORMATION:
+ has_vendor_specific_information = true;
+ break;
+ case SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_CLASS:
+ has_vendor_identifying_vendor_class = true;
+ break;
+ case SD_DHCP_OPTION_VENDOR_IDENTIFYING_VENDOR_SPECIFIC_INFORMATION:
+ has_vendor_identifying_vendor_specific_information = true;
+ break;
+ }
+ }
+
+ if (FLAGS_SET(flags, DUMP_DHCP_MESSAGE_LEGEND))
+ printf("%s%sOptions:%s\n", ansi_highlight(), ansi_add_underline(), ansi_normal());
+
+ r = table_print_or_warn(table);
+ if (r < 0)
+ return r;
+
+ if (has_vendor_specific_information) {
+ r = dump_dhcp_option_vendor_specific_information(message, flags);
+ if (r < 0)
+ return r;
+ }
+
+ if (has_vendor_identifying_vendor_class) {
+ r = dump_dhcp_option_vendor_identifying_vendor_class(message, flags);
+ if (r < 0)
+ return r;
+ }
+
+ if (has_vendor_identifying_vendor_specific_information) {
+ r = dump_dhcp_option_vendor_identifying_vendor_specific_information(message, flags);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int dump_buffer(Table *table, const char *field, uint8_t *buf, size_t len) {
+ int r;
+
+ assert(table);
+ assert(field);
+ assert(buf);
+ assert(len > 0);
+
+ uint8_t *nul = memchr(buf, 0, len);
+ if (nul)
+ len = nul - buf;
+
+ if (len == 0)
+ return 0;
+
+ _cleanup_free_ char *str = NULL;
+ r = make_cstring(buf, len, MAKE_CSTRING_REFUSE_TRAILING_NUL, &str);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse buffer for field '%s': %m", field);
+
+ if (isempty(str))
+ return 0;
+
+ r = table_add_many(
+ table,
+ TABLE_FIELD, field,
+ TABLE_STRING, str);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int dump_dhcp_header(sd_dhcp_message *message, DumpDHCPMessageFlag flags) {
+ int r;
+
+ assert(message);
+
+ _cleanup_(table_unrefp) Table *table = table_new_vertical();
+ if (!table)
+ return log_oom();
+
+ table_apply_flags(table, flags);
+
+ TableCell *cell = table_get_cell(table, 0, 0);
+ if (!cell)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get table cell.");
+
+ (void) table_set_align_percent(table, cell, 100);
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ struct hw_addr_data hw_addr;
+ r = dhcp_message_get_hw_addr(message, &hw_addr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get hardware address from DHCP message: %m");
+
+ struct in_addr yiaddr = { .s_addr = message->header.yiaddr };
+
+ r = table_add_many(
+ table,
+ TABLE_FIELD, "Hardware Type",
+ TABLE_STRING, arphrd_to_name(message->header.htype),
+ TABLE_FIELD, "Hardware Address",
+ TABLE_STRING, HW_ADDR_TO_STR(&hw_addr),
+ TABLE_FIELD, "Client Address",
+ TABLE_IN_ADDR, &yiaddr);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (message->header.siaddr != INADDR_ANY) {
+ struct in_addr siaddr = { .s_addr = message->header.siaddr };
+
+ r = table_add_many(
+ table,
+ TABLE_FIELD, "Server Address",
+ TABLE_IN_ADDR, &siaddr);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (message->header.giaddr != INADDR_ANY) {
+ struct in_addr giaddr = { .s_addr = message->header.giaddr };
+
+ r = table_add_many(
+ table,
+ TABLE_FIELD, "Relay Agent Address",
+ TABLE_IN_ADDR, &giaddr);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ uint8_t overload = DHCP_OVERLOAD_NONE;
+ (void) dhcp_message_get_option_u8(message, SD_DHCP_OPTION_OVERLOAD, &overload);
+
+ if (!FLAGS_SET(overload, DHCP_OVERLOAD_SNAME)) {
+ r = dump_buffer(table, "Server Host Name", message->header.sname, sizeof(message->header.sname));
+ if (r < 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(overload, DHCP_OVERLOAD_FILE)) {
+ r = dump_buffer(table, "Boot File Name", message->header.file, sizeof(message->header.file));
+ if (r < 0)
+ return r;
+ }
+
+ if (FLAGS_SET(flags, DUMP_DHCP_MESSAGE_LEGEND))
+ printf("%s%sHeader:%s\n", ansi_highlight(), ansi_add_underline(), ansi_normal());
+
+ return table_print_or_warn(table);
+}
+
+int dump_dhcp_message(sd_dhcp_message *m, char * const *args, DumpDHCPMessageFlag flags) {
+ int r;
+
+ assert(m);
+
+ if (strv_isempty(args)) {
+ r = dump_dhcp_header(m, flags);
+ if (r < 0)
+ return r;
+
+ putchar('\n');
+ }
+
+ return dump_dhcp_options(m, args, flags);
+}
diff --git a/src/libsystemd-network/dhcp-message-dump.h b/src/libsystemd-network/dhcp-message-dump.h
new file mode 100644
index 00000000000..6ebd5a2c465
--- /dev/null
+++ b/src/libsystemd-network/dhcp-message-dump.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "dhcp-message.h"
+
+typedef enum {
+ DUMP_DHCP_MESSAGE_LEGEND = 1 << 0,
+ DUMP_DHCP_MESSAGE_FULL = 1 << 1,
+} DumpDHCPMessageFlag;
+
+int dump_dhcp_message(sd_dhcp_message *message, char * const *args, DumpDHCPMessageFlag flags);
diff --git a/src/libsystemd-network/meson.build b/src/libsystemd-network/meson.build
index fed2c98c710..ab1ac35409c 100644
--- a/src/libsystemd-network/meson.build
+++ b/src/libsystemd-network/meson.build
@@ -4,6 +4,7 @@ libsystemd_network_sources = files(
'arp-util.c',
'dhcp-client-send.c',
'dhcp-message.c',
+ 'dhcp-message-dump.c',
'dhcp-network.c',
'dhcp-option.c',
'dhcp-packet.c',
diff --git a/src/libsystemd-network/test-dhcp-message.c b/src/libsystemd-network/test-dhcp-message.c
index b38220aef52..f42a0bfb4f8 100644
--- a/src/libsystemd-network/test-dhcp-message.c
+++ b/src/libsystemd-network/test-dhcp-message.c
@@ -7,6 +7,7 @@
#include "alloc-util.h"
#include "dhcp-client-id-internal.h"
#include "dhcp-message.h"
+#include "dhcp-message-dump.h"
#include "dhcp-protocol.h"
#include "dhcp-route.h"
#include "dns-packet.h"
@@ -545,6 +546,9 @@ TEST(dhcp_message) {
/* send */
verify_send_udp(m, xid, &hw_addr);
verify_send_raw(m, xid, &hw_addr);
+
+ /* dump */
+ ASSERT_OK(dump_dhcp_message(m, /* args= */ NULL, DUMP_DHCP_MESSAGE_LEGEND | DUMP_DHCP_MESSAGE_FULL));
}
static void test_domains_one(size_t len, const uint8_t *data, char * const *expected) {
@@ -581,6 +585,8 @@ static void test_domains_one(size_t len, const uint8_t *data, char * const *expe
ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_SIP_SERVER, len + 1 - (len + 1) / 2, sip + (len + 1) / 2));
ASSERT_OK(dhcp_message_get_option_domains(m, SD_DHCP_OPTION_SIP_SERVER, &strv));
ASSERT_TRUE(strv_equal(strv, expected));
+
+ ASSERT_OK(dump_dhcp_message(m, STRV_MAKE("119", "120"), DUMP_DHCP_MESSAGE_LEGEND | DUMP_DHCP_MESSAGE_FULL));
}
static void test_domains_fail(size_t len, const uint8_t *data) {
@@ -804,6 +810,8 @@ TEST(dnr) {
ASSERT_EQ(resolvers[1].port, 33u);
ASSERT_NULL(resolvers[1].dohpath);
+ ASSERT_OK(dump_dhcp_message(m, STRV_MAKE("162"), DUMP_DHCP_MESSAGE_LEGEND | DUMP_DHCP_MESSAGE_FULL));
+
/* missing DoH path */
static uint8_t invalid[] = {
/* length */
@@ -875,4 +883,60 @@ TEST(dnr) {
ASSERT_ERROR(dhcp_message_get_option_dnr(m, NULL, NULL), EMSGSIZE);
}
+TEST(dump_vendor) {
+ _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
+ ASSERT_OK(dhcp_message_new(&m));
+
+ /* Vendor Specific Information (43) -- TLV */
+ static uint8_t option_43[] = {
+ 1, 6, 'm', 'o', 'd', 'e', 'm', '1',
+ 2, 6, 's', 't', 'a', 't', 'u', 's',
+ 3, 6, 1, 2, 3, 4, 5, 6,
+ };
+
+ /* Vendor Class Identifier (60) -- typically string */
+ static uint8_t option_60[] = {
+ 'n', 'e', 't', 'w', 'o', 'r', 'k', 'd',
+ };
+
+ /* User Class (77) -- length-prefixed data, typically strings */
+ static uint8_t option_77[] = {
+ 6, 'l', 'a', 'p', 't', 'o', 'p',
+ 4, 'c', 'o', 'r', 'p',
+ };
+
+ /* Vendor-Identifying Vendor Class -- length-prefixed data tagged with enterprise number */
+ static uint8_t option_124[] = {
+ 0x00, 0x00, 0x00, 0x09, /* enterprise number: 9 */
+ 13,
+ 5, 'c', 'i', 's', 'c', 'o',
+ 6, 'f', 'o', 'o', 'b', 'a', 'r',
+ 0x00, 0x00, 0x11, 0x8b, /* enterprise number: 4491 */
+ 21,
+ 9, 'd', 'o', 'c', 's', 'i', 's', '3', '.', '0',
+ 10, 'e', 'R', 'o', 'u', 't', 'e', 'r', '1', '.', '0',
+ };
+
+ /* Vendor-Identifying Vendor-Specific Information (125) -- sub TLVs with enterprise number */
+ static uint8_t option_125[] = {
+ 0x00, 0x00, 0x00, 0x09, /* enterprise number: 9 */
+ 12,
+ 1, 4, 'C', 'i', 's', 'c',
+ 2, 4, 'I', 'O', 'S', 'X',
+ 0x00, 0x00, 0x11, 0x8b, /* enterprise number: 4491 */
+ 16,
+ 1, 6, 'm', 'o', 'd', 'e', 'm', '1',
+ 2, 6, 's', 't', 'a', 't', 'u', 's',
+ };
+
+ ASSERT_OK(dhcp_message_append_option(m, 43, ELEMENTSOF(option_43), option_43));
+ ASSERT_OK(dhcp_message_append_option(m, 60, ELEMENTSOF(option_60), option_60));
+ ASSERT_OK(dhcp_message_append_option(m, 77, ELEMENTSOF(option_77), option_77));
+ ASSERT_OK(dhcp_message_append_option(m, 124, ELEMENTSOF(option_124), option_124));
+ ASSERT_OK(dhcp_message_append_option(m, 125, ELEMENTSOF(option_125), option_125));
+
+ ASSERT_OK(dump_dhcp_message(m, STRV_MAKE("43", "60", "77", "124", "125"),
+ DUMP_DHCP_MESSAGE_LEGEND | DUMP_DHCP_MESSAGE_FULL));
+}
+
DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/network/meson.build b/src/network/meson.build
index 7370d011d0a..03191024925 100644
--- a/src/network/meson.build
+++ b/src/network/meson.build
@@ -128,6 +128,7 @@ networkctl_sources = files(
'networkctl-address-label.c',
'networkctl-config-file.c',
'networkctl-description.c',
+ 'networkctl-dhcp-lease.c',
'networkctl-dump-util.c',
'networkctl-journal.c',
'networkctl-link-info.c',
diff --git a/src/network/networkctl-dhcp-lease.c b/src/network/networkctl-dhcp-lease.c
new file mode 100644
index 00000000000..465bff09799
--- /dev/null
+++ b/src/network/networkctl-dhcp-lease.c
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-json.h"
+#include "sd-netlink.h"
+#include "sd-varlink.h"
+
+#include "dhcp-message-dump.h"
+#include "log.h"
+#include "networkctl.h"
+#include "networkctl-dhcp-lease.h"
+#include "networkctl-link-info.h"
+#include "networkctl-util.h"
+#include "strv.h"
+
+int verb_dhcp_lease(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ int r;
+
+ /* networkctl dhcp-lease INTERFACE [CODE[:FORMAT] ...] */
+ assert(argc >= 2);
+
+ pager_open(arg_pager_flags);
+
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to netlink: %m");
+
+ _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *vl = NULL;
+ r = varlink_connect_networkd(&vl);
+ if (r < 0)
+ return r;
+
+ const char *ifname = argv[1];
+
+ _cleanup_(link_info_array_freep) LinkInfo *link = NULL;
+ r = acquire_link_info(vl, rtnl, STRV_MAKE(ifname), &link);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EINVAL; /* already logged in acquire_link_info(). */
+ if (r > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Interface name '%s' matches multiple interfaces.", ifname);
+
+ if (!link->dhcp_message)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA),
+ "Interface '%s' does not have DHCPv4 lease.", link->name);
+
+ if (sd_json_format_enabled(arg_json_format_flags)) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = dhcp_message_build_json(link->dhcp_message, &v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build JSON variant from DHCP message: %m");
+
+ r = sd_json_variant_dump(v, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to dump JSON variant: %m");
+
+ return 0;
+ }
+
+ DumpDHCPMessageFlag flags = 0;
+ SET_FLAG(flags, DUMP_DHCP_MESSAGE_LEGEND, arg_legend);
+ SET_FLAG(flags, DUMP_DHCP_MESSAGE_FULL, arg_full);
+
+ return dump_dhcp_message(link->dhcp_message, strv_skip(argv, 2), flags);
+}
diff --git a/src/network/networkctl-dhcp-lease.h b/src/network/networkctl-dhcp-lease.h
new file mode 100644
index 00000000000..f068df3b5ce
--- /dev/null
+++ b/src/network/networkctl-dhcp-lease.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "shared-forward.h"
+
+int verb_dhcp_lease(int argc, char *argv[], uintptr_t _data, void *userdata);
diff --git a/src/network/networkctl.c b/src/network/networkctl.c
index bdf487bf2e8..bf74f10a4a4 100644
--- a/src/network/networkctl.c
+++ b/src/network/networkctl.c
@@ -12,6 +12,7 @@
#include "networkctl.h"
#include "networkctl-address-label.h"
#include "networkctl-config-file.h"
+#include "networkctl-dhcp-lease.h"
#include "networkctl-list.h"
#include "networkctl-lldp.h"
#include "networkctl-misc.h"
@@ -42,6 +43,8 @@ VERB_SCOPE(, verb_list_links, "list", "[PATTERN...
"List links");
VERB_SCOPE(, verb_link_status, "status", "[PATTERN...]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY,
"Show link status");
+VERB_SCOPE(, verb_dhcp_lease, "dhcp-lease", "INTERFACE [CODE[:FORMAT]...]", 2, VERB_ANY, VERB_ONLINE_ONLY,
+ "Show DHCP lease");
VERB_SCOPE(, verb_link_lldp_status, "lldp", "[PATTERN...]", VERB_ANY, VERB_ANY, 0,
"Show LLDP neighbors");
VERB_SCOPE(, verb_list_address_labels, "label", NULL, 1, 1, 0,
diff --git a/tools/command_ignorelist b/tools/command_ignorelist
index 548acff8bd0..cb256d66472 100644
--- a/tools/command_ignorelist
+++ b/tools/command_ignorelist
@@ -51,6 +51,15 @@ logind.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="Kill
logind.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="InhibitDelayMaxSec="]
machine-info.xml ./refsect1[title="Options"]/refsect2[title="Machine Information"]/variablelist/varlistentry[term="PRETTY_HOSTNAME="]
machine-info.xml ./refsect1[title="Options"]/refsect2[title="Machine Information"]/variablelist/varlistentry[term="ICON_NAME="]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="auto"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="hex"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="flag"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="bool"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="uint8"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="uint16"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="time"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="string"]
+networkctl.xml ./refsect1[title="Commands"]/variablelist/varlistentry[term="\n dhcp-lease\n INTERFACE\n CODE:FORMAT\n …\n "]/listitem/variablelist/varlistentry[term="address"]
os-release.xml ./refsect1[title="Options"]/refsect2[title="General information identifying the operating system"]/variablelist/varlistentry[term="NAME="]
os-release.xml ./refsect1[title="Options"]/refsect2[title="Information about the version of the operating system"]/variablelist/varlistentry[term="VERSION="]
os-release.xml ./refsect1[title="Options"]/refsect2[title="General information identifying the operating system"]/variablelist/varlistentry[term="ID="]