/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "sd-device.h"
#include "sd-dhcp-client-id.h"
#include "sd-dhcp-protocol.h"
#include "sd-hwdb.h"
#include "sd-netlink.h"
#include "sd-network.h"
#include "sd-varlink.h"

#include "alloc-util.h"
#include "bond-util.h"
#include "bridge-util.h"
#include "errno-util.h"
#include "escape.h"
#include "extract-word.h"
#include "format-table.h"
#include "format-util.h"
#include "geneve-util.h"
#include "glyph-util.h"
#include "iovec-util.h"
#include "ipvlan-util.h"
#include "json-util.h"
#include "macvlan-util.h"
#include "netif-util.h"
#include "networkctl.h"
#include "networkctl-description.h"
#include "networkctl-dump-util.h"
#include "networkctl-journal.h"
#include "networkctl-link-info.h"
#include "networkctl-lldp.h"
#include "networkctl-status-link.h"
#include "networkctl-status-system.h"
#include "networkctl-util.h"
#include "stdio-util.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "udev-util.h"

typedef struct LeaseInfo {
        const char *address;
        struct iovec client_id;
} LeaseInfo;

static void lease_info_done(LeaseInfo *p) {
        assert(p);

        iovec_done(&p->client_id);
}

static int dump_dhcp_leases(Table *table, const char *prefix, const LinkInfo *link) {
        _cleanup_strv_free_ char **buf = NULL;
        int r;

        assert(table);
        assert(prefix);
        assert(link);

        sd_json_variant *leases;
        r = json_variant_find_object(link->description, STRV_MAKE("Interface", "DHCPServer", "Leases"), &leases);
        if (r == -ENODATA)
                return 0;
        if (r < 0)
                return r;

        static const sd_json_dispatch_field dispatch_table[] = {
                { "AddressString", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string,  offsetof(LeaseInfo, address),   SD_JSON_MANDATORY },
                { "ClientId",      SD_JSON_VARIANT_ARRAY,  json_dispatch_byte_array_iovec, offsetof(LeaseInfo, client_id), 0                 },
                {}
        };

        sd_json_variant *lease;
        JSON_VARIANT_ARRAY_FOREACH(lease, leases) {
                _cleanup_(lease_info_done) LeaseInfo info = {};
                _cleanup_free_ char *client_id = NULL;

                r = sd_json_dispatch(lease, dispatch_table, SD_JSON_LOG | SD_JSON_WARNING | SD_JSON_ALLOW_EXTENSIONS, &info);
                if (r < 0)
                        continue;

                if (info.client_id.iov_len > 0)
                        (void) sd_dhcp_client_id_to_string_from_raw(info.client_id.iov_base, info.client_id.iov_len, &client_id);

                r = strv_extendf(&buf, "%s%s%s%s",
                                 info.address,
                                 client_id ? " (to " : "",
                                 strempty(client_id),
                                 client_id ? ")" : "");
                if (r < 0)
                        return log_oom();
        }

        if (strv_isempty(buf)) {
                r = strv_extendf(&buf, "none");
                if (r < 0)
                        return log_oom();
        }

        return dump_list(table, prefix, buf);
}

static int dump_ifindexes(Table *table, const char *prefix, const int *ifindexes) {
        int r;

        assert(table);
        assert(prefix);

        if (!ifindexes)
                return 0;

        for (unsigned c = 0; ifindexes[c] > 0; c++) {
                if (c == 0)
                        r = table_add_cell(table, NULL, TABLE_FIELD, prefix);
                else
                        r = table_add_cell(table, NULL, TABLE_EMPTY, NULL);
                if (r < 0)
                        return table_log_add_error(r);

                r = table_add_cell(table, NULL, TABLE_IFINDEX, &ifindexes[c]);
                if (r < 0)
                        return table_log_add_error(r);
        }

        return 0;
}

#define DUMP_STATS_ONE(name, val_name)                                          \
        ({                                                                      \
                r = table_add_cell(table, NULL, TABLE_FIELD, name);             \
                if (r < 0)                                                      \
                        return table_log_add_error(r);                          \
                r = table_add_cell(table, NULL,                                 \
                                   info->has_stats64 ? TABLE_UINT64 : TABLE_UINT32, \
                                   info->has_stats64 ? (void*) &info->stats64.val_name : (void*) &info->stats.val_name); \
                if (r < 0)                                                      \
                        return table_log_add_error(r);                          \
        })

static int dump_statistics(Table *table, const LinkInfo *info) {
        int r;

        assert(table);
        assert(info);

        if (!arg_stats)
                return 0;

        if (!info->has_stats64 && !info->has_stats)
                return 0;

        DUMP_STATS_ONE("Rx Packets", rx_packets);
        DUMP_STATS_ONE("Tx Packets", tx_packets);
        DUMP_STATS_ONE("Rx Bytes", rx_bytes);
        DUMP_STATS_ONE("Tx Bytes", tx_bytes);
        DUMP_STATS_ONE("Rx Errors", rx_errors);
        DUMP_STATS_ONE("Tx Errors", tx_errors);
        DUMP_STATS_ONE("Rx Dropped", rx_dropped);
        DUMP_STATS_ONE("Tx Dropped", tx_dropped);
        DUMP_STATS_ONE("Multicast Packets", multicast);
        DUMP_STATS_ONE("Collisions", collisions);

        return 0;
}

static int dump_hw_address(Table *table, sd_hwdb *hwdb, const char *field, const struct hw_addr_data *addr) {
        _cleanup_free_ char *description = NULL;
        int r;

        assert(table);
        assert(field);
        assert(addr);

        if (addr->length == ETH_ALEN)
                (void) ieee_oui(hwdb, &addr->ether, &description);

        r = table_add_cell(table, NULL, TABLE_FIELD, field);
        if (r < 0)
                return table_log_add_error(r);

        r = table_add_cell_stringf(table, NULL, "%s%s%s%s",
                                   HW_ADDR_TO_STR(addr),
                                   description ? " (" : "",
                                   strempty(description),
                                   description ? ")" : "");
        if (r < 0)
                return table_log_add_error(r);

        return 0;
}

static int table_add_string_line(Table *table, const char *key, const char *value) {
        int r;

        assert(table);
        assert(key);

        if (isempty(value))
                return 0;

        r = table_add_many(table,
                           TABLE_FIELD, key,
                           TABLE_STRING, value);
        if (r < 0)
                return table_log_add_error(r);

        return 0;
}

static int format_config_files(char ***files, const char *main_config) {
        assert(files);

        STRV_FOREACH(d, *files) {
                _cleanup_free_ char *s = NULL;
                Glyph tree = *(d + 1) == NULL ? GLYPH_TREE_RIGHT : GLYPH_TREE_BRANCH;

                s = strjoin(glyph(tree), *d);
                if (!s)
                        return log_oom();

                free_and_replace(*d, s);
        }

        if (strv_prepend(files, main_config) < 0)
                return log_oom();

        return 0;
}

static int link_status_one(
                sd_netlink *rtnl,
                sd_hwdb *hwdb,
                sd_varlink *vl,
                const LinkInfo *info) {

        _cleanup_strv_free_ char **dns = NULL, **ntp = NULL, **sip = NULL, **search_domains = NULL,
                **route_domains = NULL, **link_dropins = NULL, **network_dropins = NULL, **netdev_dropins = NULL;
        _cleanup_free_ char *t = NULL, *network = NULL, *netdev = NULL, *iaid = NULL, *duid = NULL, *captive_portal = NULL,
                *setup_state = NULL, *operational_state = NULL, *online_state = NULL, *activation_policy = NULL;
        const char *driver = NULL, *path = NULL, *vendor = NULL, *model = NULL, *link = NULL,
                *on_color_operational, *off_color_operational, *on_color_setup, *off_color_setup, *on_color_online;
        _cleanup_free_ int *carrier_bound_to = NULL, *carrier_bound_by = NULL;
        _cleanup_(table_unrefp) Table *table = NULL;
        int r;

        assert(rtnl);
        assert(vl);
        assert(info);

        (void) sd_network_link_get_operational_state(info->ifindex, &operational_state);
        operational_state_to_color(info->name, operational_state, &on_color_operational, &off_color_operational);

        (void) sd_network_link_get_online_state(info->ifindex, &online_state);
        online_state_to_color(online_state, &on_color_online, NULL);

        (void) sd_network_link_get_setup_state(info->ifindex, &setup_state);
        setup_state_to_color(setup_state, &on_color_setup, &off_color_setup);

        (void) sd_network_link_get_dns(info->ifindex, &dns);
        (void) sd_network_link_get_search_domains(info->ifindex, &search_domains);
        (void) sd_network_link_get_route_domains(info->ifindex, &route_domains);
        (void) sd_network_link_get_ntp(info->ifindex, &ntp);
        (void) sd_network_link_get_sip(info->ifindex, &sip);
        (void) sd_network_link_get_captive_portal(info->ifindex, &captive_portal);
        (void) sd_network_link_get_network_file(info->ifindex, &network);
        (void) sd_network_link_get_network_file_dropins(info->ifindex, &network_dropins);
        (void) sd_network_link_get_netdev_file(info->ifindex, &netdev);
        (void) sd_network_link_get_netdev_file_dropins(info->ifindex, &netdev_dropins);
        (void) sd_network_link_get_carrier_bound_to(info->ifindex, &carrier_bound_to);
        (void) sd_network_link_get_carrier_bound_by(info->ifindex, &carrier_bound_by);
        (void) sd_network_link_get_activation_policy(info->ifindex, &activation_policy);

        if (info->sd_device) {
                const char *joined;

                (void) sd_device_get_property_value(info->sd_device, "ID_NET_LINK_FILE", &link);

                if (sd_device_get_property_value(info->sd_device, "ID_NET_LINK_FILE_DROPINS", &joined) >= 0) {
                        r = strv_split_full(&link_dropins, joined, ":", EXTRACT_CUNESCAPE);
                        if (r < 0)
                                return r;
                }

                (void) sd_device_get_property_value(info->sd_device, "ID_NET_DRIVER", &driver);
                (void) sd_device_get_property_value(info->sd_device, "ID_PATH", &path);
                (void) device_get_vendor_string(info->sd_device, &vendor);
                (void) device_get_model_string(info->sd_device, &model);
        }

        r = net_get_type_string(info->sd_device, info->iftype, &t);
        if (r == -ENOMEM)
                return log_oom();

        r = format_config_files(&network_dropins, network);
        if (r < 0)
                return r;

        r = format_config_files(&link_dropins, link);
        if (r < 0)
                return r;

        r = format_config_files(&netdev_dropins, netdev);
        if (r < 0)
                return r;

        table = table_new_vertical();
        if (!table)
                return log_oom();

        if (arg_full)
                table_set_width(table, 0);

        /* Config files and basic states. */
        if (netdev_dropins) {
                r = table_add_many(table,
                                   TABLE_FIELD, "NetDev File",
                                   TABLE_STRV, netdev_dropins);
                if (r < 0)
                        return table_log_add_error(r);
        }

        r = table_add_many(table,
                           TABLE_FIELD, "Link File",
                           TABLE_STRV, link_dropins ?: STRV_MAKE("n/a"),
                           TABLE_FIELD, "Network File",
                           TABLE_STRV, network_dropins ?: STRV_MAKE("n/a"),
                           TABLE_FIELD, "State");
        if (r < 0)
                return table_log_add_error(r);

        r = table_add_cell_stringf(table, NULL, "%s%s%s (%s%s%s)",
                                   on_color_operational, strna(operational_state), off_color_operational,
                                   on_color_setup, setup_state ?: "unmanaged", off_color_setup);
        if (r < 0)
                return table_log_add_error(r);

        r = table_add_many(table,
                           TABLE_FIELD, "Online state",
                           TABLE_STRING, online_state ?: "unknown",
                           TABLE_SET_COLOR, on_color_online);
        if (r < 0)
                return table_log_add_error(r);

        r = table_add_string_line(table, "Type", t);
        if (r < 0)
                return r;

        r = table_add_string_line(table, "Kind", info->netdev_kind);
        if (r < 0)
                return r;

        r = table_add_string_line(table, "Path", path);
        if (r < 0)
                return r;

        r = table_add_string_line(table, "Driver", driver);
        if (r < 0)
                return r;

        r = table_add_string_line(table, "Vendor", vendor);
        if (r < 0)
                return r;

        r = table_add_string_line(table, "Model", model);
        if (r < 0)
                return r;

        strv_sort(info->alternative_names);
        r = dump_list(table, "Alternative Names", info->alternative_names);
        if (r < 0)
                return r;

        if (info->has_hw_address) {
                r = dump_hw_address(table, hwdb, "Hardware Address", &info->hw_address);
                if (r < 0)
                        return r;
        }

        if (info->has_permanent_hw_address) {
                r = dump_hw_address(table, hwdb, "Permanent Hardware Address", &info->permanent_hw_address);
                if (r < 0)
                        return r;
        }

        if (info->mtu > 0) {
                char min_str[DECIMAL_STR_MAX(uint32_t)], max_str[DECIMAL_STR_MAX(uint32_t)];

                xsprintf(min_str, "%" PRIu32, info->min_mtu);
                xsprintf(max_str, "%" PRIu32, info->max_mtu);

                r = table_add_cell(table, NULL, TABLE_FIELD, "MTU");
                if (r < 0)
                        return table_log_add_error(r);

                r = table_add_cell_stringf(table, NULL, "%" PRIu32 "%s%s%s%s%s%s%s",
                                           info->mtu,
                                           info->min_mtu > 0 || info->max_mtu > 0 ? " (" : "",
                                           info->min_mtu > 0 ? "min: " : "",
                                           info->min_mtu > 0 ? min_str : "",
                                           info->min_mtu > 0 && info->max_mtu > 0 ? ", " : "",
                                           info->max_mtu > 0 ? "max: " : "",
                                           info->max_mtu > 0 ? max_str : "",
                                           info->min_mtu > 0 || info->max_mtu > 0 ? ")" : "");
                if (r < 0)
                        return table_log_add_error(r);
        }

        r = table_add_string_line(table, "QDisc", info->qdisc);
        if (r < 0)
                return r;

        if (info->master > 0) {
                r = table_add_many(table,
                                   TABLE_FIELD, "Master",
                                   TABLE_IFINDEX, info->master);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (info->has_ipv6_address_generation_mode) {
                static const struct {
                        const char *mode;
                } mode_table[] = {
                        { "eui64" },
                        { "none" },
                        { "stable-privacy" },
                        { "random" },
                };

                r = table_add_many(table,
                                   TABLE_FIELD, "IPv6 Address Generation Mode",
                                   TABLE_STRING, mode_table[info->addr_gen_mode]);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (streq_ptr(info->netdev_kind, "bridge")) {
                r = table_add_many(table,
                                   TABLE_FIELD, "Forward Delay",
                                   TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->forward_delay),
                                   TABLE_FIELD, "Hello Time",
                                   TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->hello_time),
                                   TABLE_FIELD, "Max Age",
                                   TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->max_age),
                                   TABLE_FIELD, "Ageing Time",
                                   TABLE_TIMESPAN_MSEC, jiffies_to_usec(info->ageing_time),
                                   TABLE_FIELD, "Priority",
                                   TABLE_UINT16, info->priority,
                                   TABLE_FIELD, "STP",
                                   TABLE_BOOLEAN, info->stp_state > 0,
                                   TABLE_FIELD, "Multicast IGMP Version",
                                   TABLE_UINT8, info->mcast_igmp_version,
                                   TABLE_FIELD, "Cost",
                                   TABLE_UINT32, info->cost);
                if (r < 0)
                        return table_log_add_error(r);

                if (info->has_fdb_learned) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "FDB Learned",
                                           TABLE_UINT32, info->fdb_n_learned,
                                           TABLE_FIELD, "FDB Max Learned",
                                           TABLE_UINT32, info->fdb_max_learned);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (info->port_state <= BR_STATE_BLOCKING) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Port State",
                                           TABLE_STRING, bridge_state_to_string(info->port_state));
                        if (r < 0)
                                return table_log_add_error(r);
                }

        } else if (streq_ptr(info->netdev_kind, "bond")) {
                r = table_add_many(table,
                                   TABLE_FIELD, "Mode",
                                   TABLE_STRING, bond_mode_to_string(info->mode),
                                   TABLE_FIELD, "Miimon",
                                   TABLE_TIMESPAN_MSEC, info->miimon * USEC_PER_MSEC,
                                   TABLE_FIELD, "Updelay",
                                   TABLE_TIMESPAN_MSEC, info->updelay * USEC_PER_MSEC,
                                   TABLE_FIELD, "Downdelay",
                                   TABLE_TIMESPAN_MSEC, info->downdelay * USEC_PER_MSEC);
                if (r < 0)
                        return table_log_add_error(r);

        } else if (streq_ptr(info->netdev_kind, "vxlan")) {
                char ttl[CONST_MAX(STRLEN("auto") + 1, DECIMAL_STR_MAX(uint8_t))];

                if (info->vxlan_info.vni > 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "VNI",
                                           TABLE_UINT32, info->vxlan_info.vni);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (IN_SET(info->vxlan_info.group_family, AF_INET, AF_INET6)) {
                        const char *p;

                        r = in_addr_is_multicast(info->vxlan_info.group_family, &info->vxlan_info.group);
                        if (r <= 0)
                                p = "Remote";
                        else
                                p = "Group";

                        r = table_add_many(table,
                                           TABLE_FIELD, p,
                                           info->vxlan_info.group_family == AF_INET ? TABLE_IN_ADDR : TABLE_IN6_ADDR, &info->vxlan_info.group);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (IN_SET(info->vxlan_info.local_family, AF_INET, AF_INET6)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Local",
                                           info->vxlan_info.local_family == AF_INET ? TABLE_IN_ADDR : TABLE_IN6_ADDR, &info->vxlan_info.local);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (info->vxlan_info.dest_port > 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Destination Port",
                                           TABLE_UINT16, be16toh(info->vxlan_info.dest_port));
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (info->vxlan_info.link > 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Underlying Device",
                                           TABLE_IFINDEX, info->vxlan_info.link);
                        if (r < 0)
                                 return table_log_add_error(r);
                }

                r = table_add_many(table,
                                   TABLE_FIELD, "Learning",
                                   TABLE_BOOLEAN, info->vxlan_info.learning,
                                   TABLE_FIELD, "RSC",
                                   TABLE_BOOLEAN, info->vxlan_info.rsc,
                                   TABLE_FIELD, "L3MISS",
                                   TABLE_BOOLEAN, info->vxlan_info.l3miss,
                                   TABLE_FIELD, "L2MISS",
                                   TABLE_BOOLEAN, info->vxlan_info.l2miss);
                if (r < 0)
                        return table_log_add_error(r);

                if (info->vxlan_info.tos > 1) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "TOS",
                                           TABLE_UINT8, info->vxlan_info.tos);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (info->vxlan_info.ttl > 0)
                        xsprintf(ttl, "%" PRIu8, info->vxlan_info.ttl);
                else
                        strcpy(ttl, "auto");

                r = table_add_many(table,
                                   TABLE_FIELD, "TTL",
                                   TABLE_STRING, ttl);
                if (r < 0)
                        return table_log_add_error(r);

        } else if (streq_ptr(info->netdev_kind, "vlan") && info->vlan_id > 0) {
                r = table_add_many(table,
                                   TABLE_FIELD, "VLAN ID",
                                   TABLE_UINT16, info->vlan_id);
                if (r < 0)
                        return table_log_add_error(r);

        } else if (STRPTR_IN_SET(info->netdev_kind, "ipip", "sit", "gre", "gretap", "erspan", "vti")) {
                if (in_addr_is_set(AF_INET, &info->local)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Local",
                                           TABLE_IN_ADDR, &info->local);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (in_addr_is_set(AF_INET, &info->remote)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Remote",
                                           TABLE_IN_ADDR, &info->remote);
                        if (r < 0)
                                return table_log_add_error(r);
                }

        } else if (STRPTR_IN_SET(info->netdev_kind, "ip6gre", "ip6gretap", "ip6erspan", "vti6")) {
                if (in_addr_is_set(AF_INET6, &info->local)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Local",
                                           TABLE_IN6_ADDR, &info->local);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (in_addr_is_set(AF_INET6, &info->remote)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Remote",
                                           TABLE_IN6_ADDR, &info->remote);
                        if (r < 0)
                                return table_log_add_error(r);
                }

        } else if (streq_ptr(info->netdev_kind, "geneve")) {
                r = table_add_many(table,
                                   TABLE_FIELD, "VNI",
                                   TABLE_UINT32, info->vni);
                if (r < 0)
                        return table_log_add_error(r);

                if (info->has_tunnel_ipv4 && in_addr_is_set(AF_INET, &info->remote)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Remote",
                                           TABLE_IN_ADDR, &info->remote);
                        if (r < 0)
                                return table_log_add_error(r);
                } else if (in_addr_is_set(AF_INET6, &info->remote)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Remote",
                                           TABLE_IN6_ADDR, &info->remote);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (info->ttl > 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "TTL",
                                           TABLE_UINT8, info->ttl);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (info->tos > 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "TOS",
                                           TABLE_UINT8, info->tos);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                r = table_add_many(table,
                                   TABLE_FIELD, "Port",
                                   TABLE_UINT16, info->tunnel_port,
                                   TABLE_FIELD, "Inherit",
                                   TABLE_STRING, geneve_df_to_string(info->inherit));
                if (r < 0)
                        return table_log_add_error(r);

                if (info->df > 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "IPDoNotFragment",
                                           TABLE_UINT8, info->df);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                r = table_add_many(table,
                                   TABLE_FIELD, "UDPChecksum",
                                   TABLE_BOOLEAN, info->csum,
                                   TABLE_FIELD, "UDP6ZeroChecksumTx",
                                   TABLE_BOOLEAN, info->csum6_tx,
                                   TABLE_FIELD, "UDP6ZeroChecksumRx",
                                   TABLE_BOOLEAN, info->csum6_rx);
                if (r < 0)
                        return table_log_add_error(r);

                if (info->label > 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "FlowLabel",
                                           TABLE_UINT32, info->label);
                        if (r < 0)
                                return table_log_add_error(r);
                }

        } else if (STRPTR_IN_SET(info->netdev_kind, "macvlan", "macvtap")) {
                r = table_add_many(table,
                                   TABLE_FIELD, "Mode",
                                   TABLE_STRING, macvlan_mode_to_string(info->macvlan_mode));
                if (r < 0)
                        return table_log_add_error(r);

        } else if (streq_ptr(info->netdev_kind, "ipvlan")) {
                const char *p;

                if (info->ipvlan_flags & IPVLAN_F_PRIVATE)
                        p = "private";
                else if (info->ipvlan_flags & IPVLAN_F_VEPA)
                        p = "vepa";
                else
                        p = "bridge";

                r = table_add_cell(table, NULL, TABLE_FIELD, "Mode");
                if (r < 0)
                        return table_log_add_error(r);

                r = table_add_cell_stringf(table, NULL, "%s (%s)",
                                           ipvlan_mode_to_string(info->ipvlan_mode), p);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (info->has_wlan_link_info) {
                _cleanup_free_ char *esc = NULL;

                r = table_add_cell(table, NULL, TABLE_FIELD, "Wi-Fi access point");
                if (r < 0)
                        return table_log_add_error(r);

                if (info->ssid)
                        esc = cescape(info->ssid);

                r = table_add_cell_stringf(table, NULL, "%s (%s)",
                                           strnull(esc),
                                           ETHER_ADDR_TO_STR(&info->bssid));
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (info->has_bitrates) {
                r = table_add_cell(table, NULL, TABLE_FIELD, "Bit Rate (Tx/Rx)");
                if (r < 0)
                        return table_log_add_error(r);

                r = table_add_cell_stringf(table, NULL, "%sBps/%sBps",
                                           FORMAT_BYTES_FULL(info->tx_bitrate, 0),
                                           FORMAT_BYTES_FULL(info->rx_bitrate, 0));
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (info->has_tx_queues || info->has_rx_queues) {
                r = table_add_cell(table, NULL, TABLE_FIELD, "Number of Queues (Tx/Rx)");
                if (r < 0)
                        return table_log_add_error(r);

                r = table_add_cell_stringf(table, NULL, "%" PRIu32 "/%" PRIu32, info->tx_queues, info->rx_queues);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (info->has_ethtool_link_info) {
                if (IN_SET(info->autonegotiation, AUTONEG_DISABLE, AUTONEG_ENABLE)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Auto negotiation",
                                           TABLE_BOOLEAN, info->autonegotiation == AUTONEG_ENABLE);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                if (info->speed > 0 && info->speed != UINT64_MAX) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Speed",
                                           TABLE_BPS, info->speed);
                        if (r < 0)
                                return table_log_add_error(r);
                }

                r = table_add_string_line(table, "Duplex", duplex_to_string(info->duplex));
                if (r < 0)
                        return r;

                r = table_add_string_line(table, "Port", port_to_string(info->port));
                if (r < 0)
                        return r;
        }

        r = dump_addresses(rtnl, info->dhcp_message, table, info->ifindex);
        if (r < 0)
                return r;

        r = dump_gateways(rtnl, hwdb, table, info->ifindex);
        if (r < 0)
                return r;

        r = dump_list(table, "DNS", dns);
        if (r < 0)
                return r;

        r = dump_list(table, "Search Domains", search_domains);
        if (r < 0)
                return r;

        r = dump_list(table, "Route Domains", route_domains);
        if (r < 0)
                return r;

        r = dump_list(table, "NTP", ntp);
        if (r < 0)
                return r;

        r = dump_list(table, "SIP", sip);
        if (r < 0)
                return r;

        r = dump_ifindexes(table, "Carrier Bound To", carrier_bound_to);
        if (r < 0)
                return r;

        r = dump_ifindexes(table, "Carrier Bound By", carrier_bound_by);
        if (r < 0)
                return r;

        r = table_add_string_line(table, "Activation Policy", activation_policy);
        if (r < 0)
                return r;

        r = sd_network_link_get_required_for_online(info->ifindex);
        if (r >= 0) {
                r = table_add_many(table,
                                   TABLE_FIELD, "Required For Online",
                                   TABLE_BOOLEAN, r);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (captive_portal) {
                r = table_add_many(table,
                                   TABLE_FIELD, "Captive Portal",
                                   TABLE_STRING, captive_portal,
                                   TABLE_SET_URL, captive_portal);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (info->dhcp_message) {
                _cleanup_free_ char *tz = NULL;

                if (dhcp_message_get_option_string(info->dhcp_message, SD_DHCP_OPTION_TZDB_TIMEZONE, &tz) >= 0
                    && timezone_is_valid(tz, LOG_DEBUG)) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "Time Zone",
                                           TABLE_STRING, tz);
                        if (r < 0)
                                return table_log_add_error(r);
                }
        }

        if (sd_dhcp_client_id_is_set(&info->dhcp_client_id)) {
                _cleanup_free_ char *id = NULL;

                r = sd_dhcp_client_id_to_string(&info->dhcp_client_id, &id);
                if (r >= 0) {
                        r = table_add_many(table,
                                           TABLE_FIELD, "DHCPv4 Client ID",
                                           TABLE_STRING, id);
                        if (r < 0)
                                return table_log_add_error(r);
                }
        }

        r = sd_network_link_get_dhcp6_client_iaid_string(info->ifindex, &iaid);
        if (r >= 0) {
                r = table_add_many(table,
                                   TABLE_FIELD, "DHCPv6 Client IAID",
                                   TABLE_STRING, iaid);
                if (r < 0)
                        return table_log_add_error(r);
        }

        r = sd_network_link_get_dhcp6_client_duid_string(info->ifindex, &duid);
        if (r >= 0) {
                r = table_add_many(table,
                                   TABLE_FIELD, "DHCPv6 Client DUID",
                                   TABLE_STRING, duid);
                if (r < 0)
                        return table_log_add_error(r);
        }

        r = dump_lldp_neighbors(vl, table, info->ifindex);
        if (r < 0)
                return r;

        r = dump_dhcp_leases(table, "Offered DHCP leases", info);
        if (r < 0)
                return r;

        r = dump_statistics(table, info);
        if (r < 0)
                return r;

        /* First line: circle, ifindex, ifname. */
        printf("%s%s%s %d: %s\n",
               on_color_operational, glyph(GLYPH_BLACK_CIRCLE), off_color_operational,
               info->ifindex, info->name);

        r = table_print_or_warn(table);
        if (r < 0)
                return r;

        return show_logs(info->ifindex, info->name);
}

int verb_link_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
        _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
        _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL;
        _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *vl = NULL;
        _cleanup_(link_info_array_freep) LinkInfo *links = NULL;
        int r, c;

        r = dump_description(argc, argv);
        if (r != 0)
                return r;

        pager_open(arg_pager_flags);

        r = sd_netlink_open(&rtnl);
        if (r < 0)
                return log_error_errno(r, "Failed to connect to netlink: %m");

        r = sd_hwdb_new(&hwdb);
        if (r < 0)
                log_debug_errno(r, "Failed to open hardware database: %m");

        r = varlink_connect_networkd(&vl);
        if (r < 0)
                return r;

        if (arg_all)
                c = acquire_link_info(vl, rtnl, NULL, &links);
        else if (argc <= 1)
                return system_status(rtnl, hwdb);
        else
                c = acquire_link_info(vl, rtnl, argv + 1, &links);
        if (c < 0)
                return c;

        r = 0;

        bool first = true;
        FOREACH_ARRAY(i, links, c) {
                if (!first)
                        putchar('\n');

                RET_GATHER(r, link_status_one(rtnl, hwdb, vl, i));

                first = false;
        }

        return r;
}
