blob: 2b822af01a90781f68328a55b7552620d12ec8d7 [file] [log] [blame]
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <arpa/inet.h>
#include <linux/sockios.h>
#include <sys/ioctl.h>
#include "sd-event.h"
#include "sd-id128.h"
#include "sd-lldp-tx.h"
#include "alloc-util.h"
#include "ether-addr-util.h"
#include "fd-util.h"
#include "hostname-util.h"
#include "network-common.h"
#include "random-util.h"
#include "socket-util.h"
#include "string-util.h"
#include "time-util.h"
#include "unaligned.h"
#include "web-util.h"
/* The LLDP spec calls this "txFastInit", see 9.2.5.19 */
#define LLDP_FAST_TX_INIT 4U
/* The LLDP spec calls this "msgTxHold", see 9.2.5.6 */
#define LLDP_TX_HOLD 4U
/* The jitter range to add, see 9.2.2. */
#define LLDP_TX_JITTER_USEC (400U * USEC_PER_MSEC)
/* The LLDP spec calls this msgTxInterval, but we subtract half the jitter off it. */
#define LLDP_TX_INTERVAL_USEC (30U * USEC_PER_SEC - LLDP_TX_JITTER_USEC / 2)
/* The LLDP spec calls this msgFastTx, but we subtract half the jitter off it. */
#define LLDP_FAST_TX_INTERVAL_USEC (1U * USEC_PER_SEC - LLDP_TX_JITTER_USEC / 2)
#define LLDP_TX_TTL ((uint16_t) DIV_ROUND_UP(LLDP_TX_INTERVAL_USEC * LLDP_TX_HOLD + 1, USEC_PER_SEC))
static const struct ether_addr lldp_multicast_addr[_SD_LLDP_MULTICAST_MODE_MAX] = {
[SD_LLDP_MULTICAST_MODE_NEAREST_BRIDGE] = {{ 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e }},
[SD_LLDP_MULTICAST_MODE_NON_TPMR_BRIDGE] = {{ 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03 }},
[SD_LLDP_MULTICAST_MODE_CUSTOMER_BRIDGE] = {{ 0x01, 0x80, 0xc2, 0x00, 0x00, 0x00 }},
};
struct sd_lldp_tx {
unsigned n_ref;
int ifindex;
char *ifname;
sd_event *event;
int64_t event_priority;
sd_event_source *timer_event_source;
unsigned fast_tx;
sd_lldp_multicast_mode_t mode;
struct ether_addr hwaddr;
char *port_description;
char *hostname;
char *pretty_hostname;
char *mud_url;
uint16_t supported_capabilities;
uint16_t enabled_capabilities;
};
#define log_lldp_tx_errno(lldp_tx, error, fmt, ...) \
log_interface_prefix_full_errno( \
"LLDP Tx: ", \
sd_lldp_tx, lldp_tx, \
error, fmt, ##__VA_ARGS__)
#define log_lldp_tx(lldp_tx, fmt, ...) \
log_interface_prefix_full_errno_zerook( \
"LLDP Tx: ", \
sd_lldp_tx, lldp_tx, \
0, fmt, ##__VA_ARGS__)
static sd_lldp_tx *lldp_tx_free(sd_lldp_tx *lldp_tx) {
if (!lldp_tx)
return NULL;
sd_lldp_tx_detach_event(lldp_tx);
free(lldp_tx->port_description);
free(lldp_tx->hostname);
free(lldp_tx->pretty_hostname);
free(lldp_tx->mud_url);
free(lldp_tx->ifname);
return mfree(lldp_tx);
}
DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_lldp_tx, sd_lldp_tx, lldp_tx_free);
int sd_lldp_tx_new(sd_lldp_tx **ret) {
_cleanup_(sd_lldp_tx_unrefp) sd_lldp_tx *lldp_tx = NULL;
assert_return(ret, -EINVAL);
lldp_tx = new(sd_lldp_tx, 1);
if (!lldp_tx)
return -ENOMEM;
*lldp_tx = (sd_lldp_tx) {
.n_ref = 1,
.mode = _SD_LLDP_MULTICAST_MODE_INVALID,
};
*ret = TAKE_PTR(lldp_tx);
return 0;
}
int sd_lldp_tx_set_ifindex(sd_lldp_tx *lldp_tx, int ifindex) {
assert_return(lldp_tx, -EINVAL);
assert_return(ifindex > 0, -EINVAL);
lldp_tx->ifindex = ifindex;
return 0;
}
int sd_lldp_tx_set_ifname(sd_lldp_tx *lldp_tx, const char *ifname) {
assert_return(lldp_tx, -EINVAL);
assert_return(ifname, -EINVAL);
if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE))
return -EINVAL;
return free_and_strdup(&lldp_tx->ifname, ifname);
}
int sd_lldp_tx_get_ifname(sd_lldp_tx *lldp_tx, const char **ret) {
int r;
assert_return(lldp_tx, -EINVAL);
r = get_ifname(lldp_tx->ifindex, &lldp_tx->ifname);
if (r < 0)
return r;
if (ret)
*ret = lldp_tx->ifname;
return 0;
}
int sd_lldp_tx_set_multicast_mode(sd_lldp_tx *lldp_tx, sd_lldp_multicast_mode_t mode) {
assert_return(lldp_tx, -EINVAL);
assert_return(mode >= 0 && mode < _SD_LLDP_MULTICAST_MODE_MAX, -EINVAL);
lldp_tx->mode = mode;
return 0;
}
int sd_lldp_tx_set_hwaddr(sd_lldp_tx *lldp_tx, const struct ether_addr *hwaddr) {
assert_return(lldp_tx, -EINVAL);
assert_return(!ether_addr_is_null(hwaddr), -EINVAL);
lldp_tx->hwaddr = *hwaddr;
return 0;
}
int sd_lldp_tx_set_capabilities(sd_lldp_tx *lldp_tx, uint16_t supported, uint16_t enabled) {
assert_return(lldp_tx, -EINVAL);
assert_return((enabled & ~supported) == 0, -EINVAL);
lldp_tx->supported_capabilities = supported;
lldp_tx->enabled_capabilities = enabled;
return 0;
}
int sd_lldp_tx_set_port_description(sd_lldp_tx *lldp_tx, const char *port_description) {
assert_return(lldp_tx, -EINVAL);
/* An empty string unset the previously set hostname. */
if (strlen_ptr(port_description) >= 512)
return -EINVAL;
return free_and_strdup(&lldp_tx->port_description, empty_to_null(port_description));
}
int sd_lldp_tx_set_hostname(sd_lldp_tx *lldp_tx, const char *hostname) {
assert_return(lldp_tx, -EINVAL);
/* An empty string unset the previously set hostname. */
if (!isempty(hostname)) {
assert_cc(HOST_NAME_MAX < 512);
if (!hostname_is_valid(hostname, 0))
return -EINVAL;
}
return free_and_strdup(&lldp_tx->hostname, empty_to_null(hostname));
}
int sd_lldp_tx_set_pretty_hostname(sd_lldp_tx *lldp_tx, const char *pretty_hostname) {
assert_return(lldp_tx, -EINVAL);
/* An empty string unset the previously set hostname. */
if (strlen_ptr(pretty_hostname) >= 512)
return -EINVAL;
return free_and_strdup(&lldp_tx->pretty_hostname, empty_to_null(pretty_hostname));
}
int sd_lldp_tx_set_mud_url(sd_lldp_tx *lldp_tx, const char *mud_url) {
assert_return(lldp_tx, -EINVAL);
/* An empty string unset the previously set hostname. */
if (!isempty(mud_url)) {
/* Unless the maximum length of each value is 511, the MUD url must be smaller than 256.
* See RFC 8520. */
if (strlen(mud_url) >= 256)
return -EINVAL;
if (!http_url_is_valid(mud_url))
return -EINVAL;
}
return free_and_strdup(&lldp_tx->mud_url, empty_to_null(mud_url));
}
static size_t lldp_tx_calculate_maximum_packet_size(sd_lldp_tx *lldp_tx, const char *hostname, const char *pretty_hostname) {
assert(lldp_tx);
assert(lldp_tx->ifindex > 0);
return sizeof(struct ether_header) +
/* Chassis ID */
2 + 1 + (SD_ID128_STRING_MAX - 1) +
/* Port ID */
2 + 1 + strlen_ptr(lldp_tx->ifname) +
/* TTL */
2 + 2 +
/* Port description */
2 + strlen_ptr(lldp_tx->port_description) +
/* System name */
2 + strlen_ptr(hostname) +
/* System description */
2 + strlen_ptr(pretty_hostname) +
/* MUD URL */
2 + sizeof(SD_LLDP_OUI_IANA_MUD) + strlen_ptr(lldp_tx->mud_url) +
/* System Capabilities */
2 + 4 +
/* End */
2;
}
static int packet_append_tlv_header(uint8_t *packet, size_t packet_size, size_t *offset, uint8_t type, size_t data_len) {
assert(packet);
assert(offset);
/*
* +--------+--------+--------------
* |TLV Type| len | value
* |(7 bits)|(9 bits)|(0-511 octets)
* +--------+--------+--------------
* where:
*
* len = indicates the length of value
*/
/* The type field is 7-bits. */
if (type >= 128)
return -EINVAL;
/* The data length field is 9-bits. */
if (data_len >= 512)
return -EINVAL;
if (packet_size < 2 + data_len)
return -ENOBUFS;
if (*offset > packet_size - 2 - data_len)
return -ENOBUFS;
packet[(*offset)++] = (type << 1) | !!(data_len >> 8);
packet[(*offset)++] = data_len & (size_t) UINT8_MAX;
return 0;
}
static int packet_append_prefixed_string(
uint8_t *packet,
size_t packet_size,
size_t *offset,
uint8_t type,
size_t prefix_len,
const void *prefix,
const char *str) {
size_t len;
int r;
assert(packet);
assert(offset);
assert(prefix_len == 0 || prefix);
if (isempty(str))
return 0;
len = strlen(str);
/* Check for overflow */
if (len > SIZE_MAX - prefix_len)
return -ENOBUFS;
r = packet_append_tlv_header(packet, packet_size, offset, type, prefix_len + len);
if (r < 0)
return r;
memcpy_safe(packet + *offset, prefix, prefix_len);
*offset += prefix_len;
memcpy(packet + *offset, str, len);
*offset += len;
return 0;
}
static int packet_append_string(
uint8_t *packet,
size_t packet_size,
size_t *offset,
uint8_t type,
const char *str) {
return packet_append_prefixed_string(packet, packet_size, offset, type, 0, NULL, str);
}
static int lldp_tx_create_packet(sd_lldp_tx *lldp_tx, size_t *ret_packet_size, uint8_t **ret_packet) {
_cleanup_free_ char *hostname = NULL, *pretty_hostname = NULL;
_cleanup_free_ uint8_t *packet = NULL;
struct ether_header *header;
size_t packet_size, offset;
sd_id128_t machine_id;
int r;
assert(lldp_tx);
assert(lldp_tx->ifindex > 0);
assert(ret_packet_size);
assert(ret_packet);
/* If ifname is not set yet, set ifname from ifindex. */
r = sd_lldp_tx_get_ifname(lldp_tx, NULL);
if (r < 0)
return r;
r = sd_id128_get_machine(&machine_id);
if (r < 0)
return r;
if (!lldp_tx->hostname)
(void) gethostname_strict(&hostname);
if (!lldp_tx->pretty_hostname)
(void) get_pretty_hostname(&pretty_hostname);
packet_size = lldp_tx_calculate_maximum_packet_size(lldp_tx,
lldp_tx->hostname ?: hostname,
lldp_tx->pretty_hostname ?: pretty_hostname);
packet = new(uint8_t, packet_size);
if (!packet)
return -ENOMEM;
header = (struct ether_header*) packet;
header->ether_type = htobe16(ETHERTYPE_LLDP);
memcpy(header->ether_dhost, lldp_multicast_addr + lldp_tx->mode, ETH_ALEN);
memcpy(header->ether_shost, &lldp_tx->hwaddr, ETH_ALEN);
offset = sizeof(struct ether_header);
/* The three mandatory TLVs must appear first, in this specific order:
* 1. Chassis ID
* 2. Port ID
* 3. Time To Live
*/
r = packet_append_prefixed_string(packet, packet_size, &offset, SD_LLDP_TYPE_CHASSIS_ID,
1, (const uint8_t[]) { SD_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED },
SD_ID128_TO_STRING(machine_id));
if (r < 0)
return r;
r = packet_append_prefixed_string(packet, packet_size, &offset, SD_LLDP_TYPE_PORT_ID,
1, (const uint8_t[]) { SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME },
lldp_tx->ifname);
if (r < 0)
return r;
r = packet_append_tlv_header(packet, packet_size, &offset, SD_LLDP_TYPE_TTL, 2);
if (r < 0)
return r;
unaligned_write_be16(packet + offset, LLDP_TX_TTL);
offset += 2;
/* Optional TLVs follow, in no specific order: */
r = packet_append_string(packet, packet_size, &offset, SD_LLDP_TYPE_PORT_DESCRIPTION,
lldp_tx->port_description);
if (r < 0)
return r;
r = packet_append_string(packet, packet_size, &offset, SD_LLDP_TYPE_SYSTEM_NAME,
lldp_tx->hostname ?: hostname);
if (r < 0)
return r;
r = packet_append_string(packet, packet_size, &offset, SD_LLDP_TYPE_SYSTEM_DESCRIPTION,
lldp_tx->pretty_hostname ?: pretty_hostname);
if (r < 0)
return r;
/* See section 12 of RFC 8520.
* +--------+--------+----------+---------+--------------
* |TLV Type| len | OUI |subtype | MUDString
* | =127 | |= 00 00 5E| = 1 |
* |(7 bits)|(9 bits)|(3 octets)|(1 octet)|(1-255 octets)
* +--------+--------+----------+---------+--------------
* where:
*
* o TLV Type = 127 indicates a vendor-specific TLV
* o len = indicates the TLV string length
* o OUI = 00 00 5E is the organizationally unique identifier of IANA
* o subtype = 1 (as assigned by IANA for the MUDstring)
* o MUDstring = the length MUST NOT exceed 255 octets
*/
r = packet_append_prefixed_string(packet, packet_size, &offset, SD_LLDP_TYPE_PRIVATE,
sizeof(SD_LLDP_OUI_IANA_MUD), SD_LLDP_OUI_IANA_MUD,
lldp_tx->mud_url);
if (r < 0)
return r;
r = packet_append_tlv_header(packet, packet_size, &offset, SD_LLDP_TYPE_SYSTEM_CAPABILITIES, 4);
if (r < 0)
return r;
unaligned_write_be16(packet + offset, lldp_tx->supported_capabilities);
offset += 2;
unaligned_write_be16(packet + offset, lldp_tx->enabled_capabilities);
offset += 2;
r = packet_append_tlv_header(packet, packet_size, &offset, SD_LLDP_TYPE_END, 0);
if (r < 0)
return r;
*ret_packet_size = offset;
*ret_packet = TAKE_PTR(packet);
return 0;
}
static int lldp_tx_send_packet(sd_lldp_tx *lldp_tx, size_t packet_size, const uint8_t *packet) {
_cleanup_close_ int fd = -EBADF;
union sockaddr_union sa;
ssize_t l;
assert(lldp_tx);
assert(lldp_tx->ifindex > 0);
assert(packet_size > sizeof(struct ether_header));
assert(packet);
sa = (union sockaddr_union) {
.ll.sll_family = AF_PACKET,
.ll.sll_protocol = htobe16(ETHERTYPE_LLDP),
.ll.sll_ifindex = lldp_tx->ifindex,
.ll.sll_halen = ETH_ALEN,
};
memcpy(sa.ll.sll_addr, lldp_multicast_addr + lldp_tx->mode, ETH_ALEN);
fd = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_RAW);
if (fd < 0)
return -errno;
l = sendto(fd, packet, packet_size, MSG_NOSIGNAL, &sa.sa, sizeof(sa.ll));
if (l < 0)
return -errno;
if ((size_t) l != packet_size)
return -EIO;
return 0;
}
static int lldp_tx_send(sd_lldp_tx *lldp_tx) {
_cleanup_free_ uint8_t *packet = NULL;
size_t packet_size = 0; /* avoid false maybe-uninitialized warning */
int r;
assert(lldp_tx);
r = lldp_tx_create_packet(lldp_tx, &packet_size, &packet);
if (r < 0)
return r;
return lldp_tx_send_packet(lldp_tx, packet_size, packet);
}
int sd_lldp_tx_attach_event(sd_lldp_tx *lldp_tx, sd_event *event, int64_t priority) {
int r;
assert_return(lldp_tx, -EINVAL);
assert_return(!lldp_tx->event, -EBUSY);
if (event)
lldp_tx->event = sd_event_ref(event);
else {
r = sd_event_default(&lldp_tx->event);
if (r < 0)
return r;
}
lldp_tx->event_priority = priority;
return 0;
}
int sd_lldp_tx_detach_event(sd_lldp_tx *lldp_tx) {
assert_return(lldp_tx, -EINVAL);
lldp_tx->timer_event_source = sd_event_source_disable_unref(lldp_tx->timer_event_source);
lldp_tx->event = sd_event_unref(lldp_tx->event);
return 0;
}
static usec_t lldp_tx_get_delay(sd_lldp_tx *lldp_tx) {
assert(lldp_tx);
return usec_add(lldp_tx->fast_tx > 0 ? LLDP_FAST_TX_INTERVAL_USEC : LLDP_TX_INTERVAL_USEC,
(usec_t) random_u64() % LLDP_TX_JITTER_USEC);
}
static int lldp_tx_reset_timer(sd_lldp_tx *lldp_tx) {
usec_t delay;
int r;
assert(lldp_tx);
assert(lldp_tx->timer_event_source);
delay = lldp_tx_get_delay(lldp_tx);
r = sd_event_source_set_time_relative(lldp_tx->timer_event_source, delay);
if (r < 0)
return r;
return sd_event_source_set_enabled(lldp_tx->timer_event_source, SD_EVENT_ONESHOT);
}
static int on_timer_event(sd_event_source *s, uint64_t usec, void *userdata) {
sd_lldp_tx *lldp_tx = ASSERT_PTR(userdata);
int r;
r = lldp_tx_send(lldp_tx);
if (r < 0)
log_lldp_tx_errno(lldp_tx, r, "Failed to send packet, ignoring: %m");
if (lldp_tx->fast_tx > 0)
lldp_tx->fast_tx--;
r = lldp_tx_reset_timer(lldp_tx);
if (r < 0)
log_lldp_tx_errno(lldp_tx, r, "Failed to reset timer: %m");
return 0;
}
int sd_lldp_tx_is_running(sd_lldp_tx *lldp_tx) {
int enabled;
if (!lldp_tx)
return 0;
if (!lldp_tx->timer_event_source)
return 0;
if (sd_event_source_get_enabled(lldp_tx->timer_event_source, &enabled) < 0)
return 0;
return enabled == SD_EVENT_ONESHOT;
}
int sd_lldp_tx_stop(sd_lldp_tx *lldp_tx) {
if (!lldp_tx)
return 0;
if (!lldp_tx->timer_event_source)
return 0;
(void) sd_event_source_set_enabled(lldp_tx->timer_event_source, SD_EVENT_OFF);
return 1;
}
int sd_lldp_tx_start(sd_lldp_tx *lldp_tx) {
usec_t delay;
int r;
assert_return(lldp_tx, -EINVAL);
assert_return(lldp_tx->event, -EINVAL);
assert_return(lldp_tx->ifindex > 0, -EINVAL);
assert_return(lldp_tx->mode >= 0 && lldp_tx->mode < _SD_LLDP_MULTICAST_MODE_MAX, -EINVAL);
assert_return(!ether_addr_is_null(&lldp_tx->hwaddr), -EINVAL);
if (sd_lldp_tx_is_running(lldp_tx))
return 0;
lldp_tx->fast_tx = LLDP_FAST_TX_INIT;
if (lldp_tx->timer_event_source) {
r = lldp_tx_reset_timer(lldp_tx);
if (r < 0)
return log_lldp_tx_errno(lldp_tx, r, "Failed to re-enable timer: %m");
return 0;
}
delay = lldp_tx_get_delay(lldp_tx);
r = sd_event_add_time_relative(lldp_tx->event, &lldp_tx->timer_event_source,
CLOCK_BOOTTIME, delay, 0,
on_timer_event, lldp_tx);
if (r < 0)
return r;
(void) sd_event_source_set_description(lldp_tx->timer_event_source, "lldp-tx-timer");
(void) sd_event_source_set_priority(lldp_tx->timer_event_source, lldp_tx->event_priority);
return 0;
}