networkctl: introduce 'dhcp-lease' command to dump acquired DHCP lease (#42137)

This commit is contained in:
Yu Watanabe
2026-05-18 01:28:11 +09:00
committed by GitHub
13 changed files with 1465 additions and 2 deletions

6
NEWS
View File

@@ -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:

View File

@@ -287,6 +287,122 @@
</varlistentry>
<varlistentry>
<term>
<command>dhcp-lease</command>
<replaceable>INTERFACE</replaceable>
<optional><replaceable>CODE<optional><replaceable>:FORMAT</replaceable></optional></replaceable></optional>
</term>
<listitem>
<para>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.</para>
<para>When called with the <option>--json=</option> option, the entire DHCP message is shown in
JSON format. In that case, any specified option codes are ignored.</para>
<para>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.
<literal>192.0.2.1</literal>), and the lease time is shown as a human-readable duration (e.g.
<literal>8h</literal>).</para>
<para>Each option code may optionally be followed by a formatter, separated by a colon (e.g.
<literal>42:address</literal>). This can be useful if the default formatting is not appropriate.
</para>
<para>The following formatters are supported:</para>
<variablelist>
<varlistentry>
<term><option>auto</option></term>
<listitem>
<para>Use the default formatting.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>hex</option></term>
<listitem>
<para>Show the raw bytes in hexadecimal, separated by colons.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>flag</option></term>
<listitem>
<para>Treat the option as having zero-length data and display <literal>yes</literal>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>bool</option></term>
<listitem>
<para>Treat the option as a 1-byte boolean and display <literal>yes</literal> or <literal>no</literal>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>uint8</option></term>
<listitem>
<para>Treat the option as a 1-byte unsigned integer.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>uint16</option></term>
<listitem>
<para>Treat the option as a 2-byte unsigned integer.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>time</option></term>
<listitem>
<para>Treat the option as a 4-byte time value in seconds and display it as a human-readable duration.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>string</option></term>
<listitem>
<para>Treat the option as a string.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>address</option></term>
<listitem>
<para>Treat the option as one or more IPv4 addresses and display them in human-readable form.</para>
</listitem>
</varlistentry>
</variablelist>
<para>Produces output similar to:
<programlisting>$ 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</programlisting>
</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
</varlistentry>
<varlistentry>
<term>
<command>lldp</command>

View File

@@ -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'

View File

@@ -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@ )

File diff suppressed because it is too large Load Diff

View File

@@ -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);

View File

@@ -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',

View File

@@ -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);

View File

@@ -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',

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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="]