diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
commit | 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch) | |
tree | 33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/libsystemd-network | |
parent | Initial commit. (diff) | |
download | systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip |
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/libsystemd-network')
73 files changed, 23338 insertions, 0 deletions
diff --git a/src/libsystemd-network/arp-util.c b/src/libsystemd-network/arp-util.c new file mode 100644 index 0000000..ad61614 --- /dev/null +++ b/src/libsystemd-network/arp-util.c @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Axis Communications AB. All rights reserved. +***/ + +#include <arpa/inet.h> +#include <linux/filter.h> +#include <netinet/if_ether.h> + +#include "arp-util.h" +#include "ether-addr-util.h" +#include "fd-util.h" +#include "in-addr-util.h" +#include "unaligned.h" + +int arp_update_filter(int fd, const struct in_addr *a, const struct ether_addr *mac) { + struct sock_filter filter[] = { + BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0), /* A <- packet length */ + BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct ether_arp), 1, 0), /* packet >= arp packet ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct ether_arp, ea_hdr.ar_hrd)), /* A <- header */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARPHRD_ETHER, 1, 0), /* header == ethernet ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct ether_arp, ea_hdr.ar_pro)), /* A <- protocol */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETHERTYPE_IP, 1, 0), /* protocol == IP ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct ether_arp, ea_hdr.ar_hln)), /* A <- hardware address length */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, sizeof(struct ether_addr), 1, 0), /* length == sizeof(ether_addr)? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct ether_arp, ea_hdr.ar_pln)), /* A <- protocol address length */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, sizeof(struct in_addr), 1, 0), /* length == sizeof(in_addr) ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct ether_arp, ea_hdr.ar_op)), /* A <- operation */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARPOP_REQUEST, 2, 0), /* protocol == request ? */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARPOP_REPLY, 1, 0), /* protocol == reply ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + /* Sender Hardware Address must be different from our own */ + BPF_STMT(BPF_LDX + BPF_IMM, unaligned_read_be32(&mac->ether_addr_octet[0])), /* X <- 4 bytes of client's MAC */ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct ether_arp, arp_sha)), /* A <- 4 bytes of SHA */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 0, 4), /* A == X ? */ + BPF_STMT(BPF_LDX + BPF_IMM, unaligned_read_be16(&mac->ether_addr_octet[4])), /* X <- remainder of client's MAC */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct ether_arp, arp_sha) + 4), /* A <- remainder of SHA */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 0, 1), /* A == X ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + /* Sender Protocol Address or Target Protocol Address must be equal to the one we care about */ + BPF_STMT(BPF_LDX + BPF_IMM, htobe32(a->s_addr)), /* X <- clients IP */ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct ether_arp, arp_spa)), /* A <- SPA */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 0, 1), /* A == X ? */ + BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct ether_arp, arp_tpa)), /* A <- TPA */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 0, 1), /* A == 0 ? */ + BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + }; + struct sock_fprog fprog = { + .len = ELEMENTSOF(filter), + .filter = (struct sock_filter*) filter, + }; + + assert(fd >= 0); + + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)) < 0) + return -errno; + + return 0; +} + +int arp_network_bind_raw_socket(int ifindex, const struct in_addr *a, const struct ether_addr *mac) { + union sockaddr_union link = { + .ll.sll_family = AF_PACKET, + .ll.sll_protocol = htobe16(ETH_P_ARP), + .ll.sll_ifindex = ifindex, + .ll.sll_halen = ETH_ALEN, + .ll.sll_addr = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, + }; + _cleanup_close_ int s = -EBADF; + int r; + + assert(ifindex > 0); + assert(mac); + + s = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (s < 0) + return -errno; + + r = arp_update_filter(s, a, mac); + if (r < 0) + return r; + + if (bind(s, &link.sa, sizeof(link.ll)) < 0) + return -errno; + + return TAKE_FD(s); +} + +int arp_send_packet( + int fd, + int ifindex, + const struct in_addr *pa, + const struct ether_addr *ha, + bool announce) { + + union sockaddr_union link = { + .ll.sll_family = AF_PACKET, + .ll.sll_protocol = htobe16(ETH_P_ARP), + .ll.sll_ifindex = ifindex, + .ll.sll_halen = ETH_ALEN, + .ll.sll_addr = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, + }; + struct ether_arp arp = { + .ea_hdr.ar_hrd = htobe16(ARPHRD_ETHER), /* HTYPE */ + .ea_hdr.ar_pro = htobe16(ETHERTYPE_IP), /* PTYPE */ + .ea_hdr.ar_hln = ETH_ALEN, /* HLEN */ + .ea_hdr.ar_pln = sizeof(struct in_addr), /* PLEN */ + .ea_hdr.ar_op = htobe16(ARPOP_REQUEST), /* REQUEST */ + }; + ssize_t n; + + assert(fd >= 0); + assert(ifindex > 0); + assert(pa); + assert(in4_addr_is_set(pa)); + assert(ha); + assert(!ether_addr_is_null(ha)); + + memcpy(&arp.arp_sha, ha, ETH_ALEN); + memcpy(&arp.arp_tpa, pa, sizeof(struct in_addr)); + + if (announce) + memcpy(&arp.arp_spa, pa, sizeof(struct in_addr)); + + n = sendto(fd, &arp, sizeof(struct ether_arp), 0, &link.sa, sizeof(link.ll)); + if (n < 0) + return -errno; + if (n != sizeof(struct ether_arp)) + return -EIO; + + return 0; +} diff --git a/src/libsystemd-network/arp-util.h b/src/libsystemd-network/arp-util.h new file mode 100644 index 0000000..b66a81b --- /dev/null +++ b/src/libsystemd-network/arp-util.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014 Axis Communications AB. All rights reserved. +***/ + +#include <net/ethernet.h> +#include <netinet/in.h> + +#include "socket-util.h" +#include "sparse-endian.h" + +int arp_update_filter(int fd, const struct in_addr *a, const struct ether_addr *mac); +int arp_network_bind_raw_socket(int ifindex, const struct in_addr *a, const struct ether_addr *mac); + +int arp_send_packet( + int fd, + int ifindex, + const struct in_addr *pa, + const struct ether_addr *ha, + bool announce); +static inline int arp_send_probe( + int fd, + int ifindex, + const struct in_addr *pa, + const struct ether_addr *ha) { + return arp_send_packet(fd, ifindex, pa, ha, false); +} +static inline int arp_send_announcement( + int fd, + int ifindex, + const struct in_addr *pa, + const struct ether_addr *ha) { + return arp_send_packet(fd, ifindex, pa, ha, true); +} diff --git a/src/libsystemd-network/dhcp-client-internal.h b/src/libsystemd-network/dhcp-client-internal.h new file mode 100644 index 0000000..28ce80c --- /dev/null +++ b/src/libsystemd-network/dhcp-client-internal.h @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <errno.h> + +#include "sd-dhcp-client.h" + +#include "macro.h" +#include "network-common.h" + +typedef enum DHCPState { + DHCP_STATE_STOPPED, + DHCP_STATE_INIT, + DHCP_STATE_SELECTING, + DHCP_STATE_INIT_REBOOT, + DHCP_STATE_REBOOTING, + DHCP_STATE_REQUESTING, + DHCP_STATE_BOUND, + DHCP_STATE_RENEWING, + DHCP_STATE_REBINDING, + _DHCP_STATE_MAX, + _DHCP_STATE_INVALID = -EINVAL, +} DHCPState; + +const char *dhcp_state_to_string(DHCPState s) _const_; + +typedef struct sd_dhcp_client sd_dhcp_client; + +int dhcp_client_set_state_callback( + sd_dhcp_client *client, + sd_dhcp_client_callback_t cb, + void *userdata); +int dhcp_client_get_state(sd_dhcp_client *client); + +/* If we are invoking callbacks of a dhcp-client, ensure unreffing the + * client from the callback doesn't destroy the object we are working + * on */ +#define DHCP_CLIENT_DONT_DESTROY(client) \ + _cleanup_(sd_dhcp_client_unrefp) _unused_ sd_dhcp_client *_dont_destroy_##client = sd_dhcp_client_ref(client) + +#define log_dhcp_client_errno(client, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "DHCPv4 client: ", \ + sd_dhcp_client, client, \ + error, fmt, ##__VA_ARGS__) +#define log_dhcp_client(client, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "DHCPv4 client: ", \ + sd_dhcp_client, client, \ + 0, fmt, ##__VA_ARGS__) diff --git a/src/libsystemd-network/dhcp-identifier.c b/src/libsystemd-network/dhcp-identifier.c new file mode 100644 index 0000000..f65cdbe --- /dev/null +++ b/src/libsystemd-network/dhcp-identifier.c @@ -0,0 +1,209 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if_infiniband.h> +#include <net/ethernet.h> +#include <net/if_arp.h> + +#include "dhcp-identifier.h" +#include "netif-util.h" +#include "network-common.h" +#include "siphash24.h" +#include "sparse-endian.h" +#include "string-table.h" + +#define HASH_KEY SD_ID128_MAKE(80,11,8c,c2,fe,4a,03,ee,3e,d6,0c,6f,36,39,14,09) +#define APPLICATION_ID SD_ID128_MAKE(a5,0a,d1,12,bf,60,45,77,a2,fb,74,1a,b1,95,5b,03) +#define USEC_2000 ((usec_t) 946684800000000) /* 2000-01-01 00:00:00 UTC */ + +static const char * const duid_type_table[_DUID_TYPE_MAX] = { + [DUID_TYPE_LLT] = "DUID-LLT", + [DUID_TYPE_EN] = "DUID-EN/Vendor", + [DUID_TYPE_LL] = "DUID-LL", + [DUID_TYPE_UUID] = "UUID", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(duid_type, DUIDType); + +int dhcp_identifier_set_duid_llt( + const struct hw_addr_data *hw_addr, + uint16_t arp_type, + usec_t t, + struct duid *ret_duid, + size_t *ret_len) { + + uint16_t time_from_2000y; + + assert(hw_addr); + assert(ret_duid); + assert(ret_len); + + if (hw_addr->length == 0) + return -EOPNOTSUPP; + + if (arp_type == ARPHRD_ETHER) + assert_return(hw_addr->length == ETH_ALEN, -EINVAL); + else if (arp_type == ARPHRD_INFINIBAND) + assert_return(hw_addr->length == INFINIBAND_ALEN, -EINVAL); + else + return -EOPNOTSUPP; + + if (t < USEC_2000) + time_from_2000y = 0; + else + time_from_2000y = (uint16_t) (((t - USEC_2000) / USEC_PER_SEC) & 0xffffffff); + + unaligned_write_be16(&ret_duid->type, DUID_TYPE_LLT); + unaligned_write_be16(&ret_duid->llt.htype, arp_type); + unaligned_write_be32(&ret_duid->llt.time, time_from_2000y); + memcpy(ret_duid->llt.haddr, hw_addr->bytes, hw_addr->length); + + *ret_len = offsetof(struct duid, llt.haddr) + hw_addr->length; + + return 0; +} + +int dhcp_identifier_set_duid_ll( + const struct hw_addr_data *hw_addr, + uint16_t arp_type, + struct duid *ret_duid, + size_t *ret_len) { + + assert(hw_addr); + assert(ret_duid); + assert(ret_len); + + if (hw_addr->length == 0) + return -EOPNOTSUPP; + + if (arp_type == ARPHRD_ETHER) + assert_return(hw_addr->length == ETH_ALEN, -EINVAL); + else if (arp_type == ARPHRD_INFINIBAND) + assert_return(hw_addr->length == INFINIBAND_ALEN, -EINVAL); + else + return -EOPNOTSUPP; + + unaligned_write_be16(&ret_duid->type, DUID_TYPE_LL); + unaligned_write_be16(&ret_duid->ll.htype, arp_type); + memcpy(ret_duid->ll.haddr, hw_addr->bytes, hw_addr->length); + + *ret_len = offsetof(struct duid, ll.haddr) + hw_addr->length; + + return 0; +} + +int dhcp_identifier_set_duid_en(struct duid *ret_duid, size_t *ret_len) { + sd_id128_t machine_id; + bool test_mode; + uint64_t hash; + int r; + + assert(ret_duid); + assert(ret_len); + + test_mode = network_test_mode_enabled(); + + if (!test_mode) { + r = sd_id128_get_machine(&machine_id); + if (r < 0) + return r; + } else + /* For tests, especially for fuzzers, reproducibility is important. + * Hence, use a static and constant machine ID. + * See 9216fddc5a8ac2742e6cfa7660f95c20ca4f2193. */ + machine_id = SD_ID128_MAKE(01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f, 10); + + unaligned_write_be16(&ret_duid->type, DUID_TYPE_EN); + unaligned_write_be32(&ret_duid->en.pen, SYSTEMD_PEN); + + /* a bit of snake-oil perhaps, but no need to expose the machine-id + * directly; duid->en.id might not be aligned, so we need to copy */ + hash = htole64(siphash24(&machine_id, sizeof(machine_id), HASH_KEY.bytes)); + memcpy(ret_duid->en.id, &hash, sizeof(hash)); + + *ret_len = offsetof(struct duid, en.id) + sizeof(hash); + + if (test_mode) + assert_se(memcmp(ret_duid, (const uint8_t[]) { 0x00, 0x02, 0x00, 0x00, 0xab, 0x11, 0x61, 0x77, 0x40, 0xde, 0x13, 0x42, 0xc3, 0xa2 }, *ret_len) == 0); + + return 0; +} + +int dhcp_identifier_set_duid_uuid(struct duid *ret_duid, size_t *ret_len) { + sd_id128_t machine_id; + int r; + + assert(ret_duid); + assert(ret_len); + + r = sd_id128_get_machine_app_specific(APPLICATION_ID, &machine_id); + if (r < 0) + return r; + + unaligned_write_be16(&ret_duid->type, DUID_TYPE_UUID); + memcpy(&ret_duid->uuid.uuid, &machine_id, sizeof(machine_id)); + + *ret_len = offsetof(struct duid, uuid.uuid) + sizeof(machine_id); + + return 0; +} + +int dhcp_identifier_set_duid_raw( + DUIDType duid_type, + const uint8_t *buf, + size_t buf_len, + struct duid *ret_duid, + size_t *ret_len) { + + assert(buf || buf_len == 0); + assert(ret_duid); + assert(ret_len); + + if (duid_type < 0 || duid_type > UINT16_MAX) + return -EINVAL; + + if (buf_len > MAX_DUID_DATA_LEN) + return -EINVAL; + + unaligned_write_be16(&ret_duid->type, duid_type); + memcpy_safe(ret_duid->raw.data, buf, buf_len); + + *ret_len = offsetof(struct duid, raw.data) + buf_len; + return 0; +} + +int dhcp_identifier_set_iaid( + sd_device *dev, + const struct hw_addr_data *hw_addr, + bool legacy_unstable_byteorder, + void *ret) { + + const char *name = NULL; + uint32_t id32; + uint64_t id; + + assert(hw_addr); + assert(ret); + + if (dev) + name = net_get_persistent_name(dev); + if (name) + id = siphash24(name, strlen(name), HASH_KEY.bytes); + else + /* fall back to MAC address if no predictable name available */ + id = siphash24(hw_addr->bytes, hw_addr->length, HASH_KEY.bytes); + + id32 = (id & 0xffffffff) ^ (id >> 32); + + if (legacy_unstable_byteorder) + /* for historical reasons (a bug), the bits were swapped and thus + * the result was endianness dependent. Preserve that behavior. */ + id32 = bswap_32(id32); + else + /* the fixed behavior returns a stable byte order. Since LE is expected + * to be more common, swap the bytes on LE to give the same as legacy + * behavior. */ + id32 = be32toh(id32); + + unaligned_write_ne32(ret, id32); + return 0; +} diff --git a/src/libsystemd-network/dhcp-identifier.h b/src/libsystemd-network/dhcp-identifier.h new file mode 100644 index 0000000..96db588 --- /dev/null +++ b/src/libsystemd-network/dhcp-identifier.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-device.h" +#include "sd-id128.h" + +#include "ether-addr-util.h" +#include "macro.h" +#include "sparse-endian.h" +#include "time-util.h" +#include "unaligned.h" + +#define SYSTEMD_PEN 43793 + +typedef enum DUIDType { + DUID_TYPE_LLT = 1, + DUID_TYPE_EN = 2, + DUID_TYPE_LL = 3, + DUID_TYPE_UUID = 4, + _DUID_TYPE_MAX, + _DUID_TYPE_INVALID = -EINVAL, + _DUID_TYPE_FORCE_U16 = UINT16_MAX, +} DUIDType; + +/* RFC 8415 section 11.1: + * A DUID consists of a 2-octet type code represented in network byte order, followed by a variable number of + * octets that make up the actual identifier. The length of the DUID (not including the type code) is at + * least 1 octet and at most 128 octets. */ +#define MAX_DUID_DATA_LEN 128 +#define MAX_DUID_LEN (sizeof(be16_t) + MAX_DUID_DATA_LEN) + +/* https://tools.ietf.org/html/rfc3315#section-9.1 */ +struct duid { + be16_t type; + union { + struct { + /* DUID_TYPE_LLT */ + be16_t htype; + be32_t time; + uint8_t haddr[]; + } _packed_ llt; + struct { + /* DUID_TYPE_EN */ + be32_t pen; + uint8_t id[]; + } _packed_ en; + struct { + /* DUID_TYPE_LL */ + be16_t htype; + uint8_t haddr[]; + } _packed_ ll; + struct { + /* DUID_TYPE_UUID */ + sd_id128_t uuid; + } _packed_ uuid; + struct { + uint8_t data[MAX_DUID_DATA_LEN]; + } _packed_ raw; + }; +} _packed_; + +int dhcp_identifier_set_duid_llt( + const struct hw_addr_data *hw_addr, + uint16_t arp_type, + usec_t t, + struct duid *ret_duid, + size_t *ret_len); +int dhcp_identifier_set_duid_ll( + const struct hw_addr_data *hw_addr, + uint16_t arp_type, + struct duid *ret_duid, + size_t *ret_len); +int dhcp_identifier_set_duid_en(struct duid *ret_duid, size_t *ret_len); +int dhcp_identifier_set_duid_uuid(struct duid *ret_duid, size_t *ret_len); +int dhcp_identifier_set_duid_raw( + DUIDType duid_type, + const uint8_t *buf, + size_t buf_len, + struct duid *ret_duid, + size_t *ret_len); +int dhcp_identifier_set_iaid( + sd_device *dev, + const struct hw_addr_data *hw_addr, + bool legacy_unstable_byteorder, + void *ret); + +const char *duid_type_to_string(DUIDType t) _const_; diff --git a/src/libsystemd-network/dhcp-lease-internal.h b/src/libsystemd-network/dhcp-lease-internal.h new file mode 100644 index 0000000..a3d8bb4 --- /dev/null +++ b/src/libsystemd-network/dhcp-lease-internal.h @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include "sd-dhcp-client.h" + +#include "alloc-util.h" +#include "dhcp-option.h" +#include "list.h" +#include "time-util.h" + +struct sd_dhcp_route { + struct in_addr dst_addr; + struct in_addr gw_addr; + unsigned char dst_prefixlen; +}; + +struct sd_dhcp_raw_option { + LIST_FIELDS(struct sd_dhcp_raw_option, options); + + uint8_t tag; + uint8_t length; + void *data; +}; + +struct sd_dhcp_lease { + unsigned n_ref; + + /* each 0 if unset */ + usec_t t1; + usec_t t2; + usec_t lifetime; + triple_timestamp timestamp; + usec_t ipv6_only_preferred_usec; + + /* each 0 if unset */ + be32_t address; + be32_t server_address; + be32_t next_server; + + bool have_subnet_mask; + be32_t subnet_mask; + + bool have_broadcast; + be32_t broadcast; + + struct in_addr *router; + size_t router_size; + + bool rapid_commit; + + DHCPServerData servers[_SD_DHCP_LEASE_SERVER_TYPE_MAX]; + + struct sd_dhcp_route *static_routes; + size_t n_static_routes; + struct sd_dhcp_route *classless_routes; + size_t n_classless_routes; + + uint16_t mtu; /* 0 if unset */ + + char *domainname; + char **search_domains; + char *hostname; + char *root_path; + char *captive_portal; + + void *client_id; + size_t client_id_len; + + void *vendor_specific; + size_t vendor_specific_len; + + char *timezone; + + uint8_t sixrd_ipv4masklen; + uint8_t sixrd_prefixlen; + struct in6_addr sixrd_prefix; + struct in_addr *sixrd_br_addresses; + size_t sixrd_n_br_addresses; + + LIST_HEAD(struct sd_dhcp_raw_option, private_options); +}; + +int dhcp_lease_new(sd_dhcp_lease **ret); + +int dhcp_lease_parse_options(uint8_t code, uint8_t len, const void *option, void *userdata); +int dhcp_lease_parse_search_domains(const uint8_t *option, size_t len, char ***domains); +int dhcp_lease_insert_private_option(sd_dhcp_lease *lease, uint8_t tag, const void *data, uint8_t len); + +void dhcp_lease_set_timestamp(sd_dhcp_lease *lease, const triple_timestamp *timestamp); +int dhcp_lease_set_default_subnet_mask(sd_dhcp_lease *lease); +int dhcp_lease_set_client_id(sd_dhcp_lease *lease, const void *client_id, size_t client_id_len); + +#define dhcp_lease_unref_and_replace(a, b) \ + unref_and_replace_full(a, b, sd_dhcp_lease_ref, sd_dhcp_lease_unref) diff --git a/src/libsystemd-network/dhcp-network.c b/src/libsystemd-network/dhcp-network.c new file mode 100644 index 0000000..1f4ad09 --- /dev/null +++ b/src/libsystemd-network/dhcp-network.c @@ -0,0 +1,287 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <net/ethernet.h> +#include <net/if.h> +#include <net/if_arp.h> +#include <stdio.h> +#include <string.h> +#include <linux/filter.h> +#include <linux/if_infiniband.h> +#include <linux/if_packet.h> + +#include "dhcp-network.h" +#include "dhcp-protocol.h" +#include "fd-util.h" +#include "unaligned.h" + +static int _bind_raw_socket( + int ifindex, + union sockaddr_union *link, + uint32_t xid, + const struct hw_addr_data *hw_addr, + const struct hw_addr_data *bcast_addr, + uint16_t arp_type, + uint16_t port, + bool so_priority_set, + int so_priority) { + + assert(ifindex > 0); + assert(link); + assert(hw_addr); + assert(bcast_addr); + assert(IN_SET(arp_type, ARPHRD_ETHER, ARPHRD_INFINIBAND)); + + switch (arp_type) { + case ARPHRD_ETHER: + assert(hw_addr->length == ETH_ALEN); + assert(bcast_addr->length == ETH_ALEN); + break; + case ARPHRD_INFINIBAND: + assert(hw_addr->length == 0); + assert(bcast_addr->length == INFINIBAND_ALEN); + break; + default: + assert_not_reached(); + } + + struct sock_filter filter[] = { + BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0), /* A <- packet length */ + BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(DHCPPacket), 1, 0), /* packet >= DHCPPacket ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(DHCPPacket, ip.protocol)), /* A <- IP protocol */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 1, 0), /* IP protocol == UDP ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(DHCPPacket, ip.frag_off)), /* A <- Flags */ + BPF_STMT(BPF_ALU + BPF_AND + BPF_K, 0x20), /* A <- A & 0x20 (More Fragments bit) */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0, 1, 0), /* A == 0 ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(DHCPPacket, ip.frag_off)), /* A <- Flags + Fragment offset */ + BPF_STMT(BPF_ALU + BPF_AND + BPF_K, 0x1fff), /* A <- A & 0x1fff (Fragment offset) */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0, 1, 0), /* A == 0 ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(DHCPPacket, udp.dest)), /* A <- UDP destination port */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, port, 1, 0), /* UDP destination port == DHCP client port ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(DHCPPacket, dhcp.op)), /* A <- DHCP op */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, BOOTREPLY, 1, 0), /* op == BOOTREPLY ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(DHCPPacket, dhcp.htype)), /* A <- DHCP header type */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arp_type, 1, 0), /* header type == arp_type ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(DHCPPacket, dhcp.xid)), /* A <- client identifier */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, xid, 1, 0), /* client identifier == xid ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(DHCPPacket, dhcp.hlen)), /* A <- MAC address length */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, (uint8_t) hw_addr->length, 1, 0), /* address length == hw_addr->length ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + /* We only support MAC address length to be either 0 or 6 (ETH_ALEN). Optionally + * compare chaddr for ETH_ALEN bytes. */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETH_ALEN, 0, 8), /* A (the MAC address length) == ETH_ALEN ? */ + BPF_STMT(BPF_LDX + BPF_IMM, unaligned_read_be32(hw_addr->bytes)), /* X <- 4 bytes of client's MAC */ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(DHCPPacket, dhcp.chaddr)), /* A <- 4 bytes of MAC from dhcp.chaddr */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 1, 0), /* A == X ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_LDX + BPF_IMM, unaligned_read_be16(hw_addr->bytes + 4)), /* X <- remainder of client's MAC */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(DHCPPacket, dhcp.chaddr) + 4), /* A <- remainder of MAC from dhcp.chaddr */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 1, 0), /* A == X ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(DHCPPacket, dhcp.magic)), /* A <- DHCP magic cookie */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, DHCP_MAGIC_COOKIE, 1, 0), /* cookie == DHCP magic cookie ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept */ + }; + struct sock_fprog fprog = { + .len = ELEMENTSOF(filter), + .filter = filter + }; + _cleanup_close_ int s = -EBADF; + int r; + + s = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (s < 0) + return -errno; + + r = setsockopt_int(s, SOL_PACKET, PACKET_AUXDATA, true); + if (r < 0) + return r; + + r = setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)); + if (r < 0) + return -errno; + + r = setsockopt_int(s, SOL_SOCKET, SO_TIMESTAMP, true); + if (r < 0) + return r; + + if (so_priority_set) { + r = setsockopt_int(s, SOL_SOCKET, SO_PRIORITY, so_priority); + if (r < 0) + return r; + } + + link->ll = (struct sockaddr_ll) { + .sll_family = AF_PACKET, + .sll_protocol = htobe16(ETH_P_IP), + .sll_ifindex = ifindex, + .sll_hatype = htobe16(arp_type), + .sll_halen = bcast_addr->length, + }; + /* We may overflow link->ll. link->ll_buffer ensures we have enough space. */ + memcpy(link->ll.sll_addr, bcast_addr->bytes, bcast_addr->length); + + r = bind(s, &link->sa, SOCKADDR_LL_LEN(link->ll)); + if (r < 0) + return -errno; + + return TAKE_FD(s); +} + +int dhcp_network_bind_raw_socket( + int ifindex, + union sockaddr_union *link, + uint32_t xid, + const struct hw_addr_data *hw_addr, + const struct hw_addr_data *bcast_addr, + uint16_t arp_type, + uint16_t port, + bool so_priority_set, + int so_priority) { + + static struct hw_addr_data default_eth_bcast = { + .length = ETH_ALEN, + .ether = {{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }}, + }, default_ib_bcast = { + .length = INFINIBAND_ALEN, + .infiniband = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0x12, 0x40, 0x1b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff + }, + }; + + assert(ifindex > 0); + assert(link); + assert(hw_addr); + + switch (arp_type) { + case ARPHRD_ETHER: + return _bind_raw_socket(ifindex, link, xid, + hw_addr, + (bcast_addr && !hw_addr_is_null(bcast_addr)) ? bcast_addr : &default_eth_bcast, + arp_type, port, so_priority_set, so_priority); + + case ARPHRD_INFINIBAND: + return _bind_raw_socket(ifindex, link, xid, + &HW_ADDR_NULL, + (bcast_addr && !hw_addr_is_null(bcast_addr)) ? bcast_addr : &default_ib_bcast, + arp_type, port, so_priority_set, so_priority); + default: + return -EINVAL; + } +} + +int dhcp_network_bind_udp_socket(int ifindex, be32_t address, uint16_t port, int ip_service_type) { + union sockaddr_union src = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(port), + .in.sin_addr.s_addr = address, + }; + _cleanup_close_ int s = -EBADF; + int r; + + s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (s < 0) + return -errno; + + if (ip_service_type >= 0) + r = setsockopt_int(s, IPPROTO_IP, IP_TOS, ip_service_type); + else + r = setsockopt_int(s, IPPROTO_IP, IP_TOS, IPTOS_CLASS_CS6); + if (r < 0) + return r; + + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return r; + + r = setsockopt_int(s, SOL_SOCKET, SO_TIMESTAMP, true); + if (r < 0) + return r; + + if (ifindex > 0) { + r = socket_bind_to_ifindex(s, ifindex); + if (r < 0) + return r; + } + + if (port == DHCP_PORT_SERVER) { + r = setsockopt_int(s, SOL_SOCKET, SO_BROADCAST, true); + if (r < 0) + return r; + if (address == INADDR_ANY) { + /* IP_PKTINFO filter should not be applied when packets are + allowed to enter/leave through the interface other than + DHCP server sits on(BindToInterface option). */ + r = setsockopt_int(s, IPPROTO_IP, IP_PKTINFO, true); + if (r < 0) + return r; + } + } else { + r = setsockopt_int(s, IPPROTO_IP, IP_FREEBIND, true); + if (r < 0) + return r; + } + + if (bind(s, &src.sa, sizeof(src.in)) < 0) + return -errno; + + return TAKE_FD(s); +} + +int dhcp_network_send_raw_socket( + int s, + const union sockaddr_union *link, + const void *packet, + size_t len) { + + /* Do not add assert(s >= 0) here, as this is called in fuzz-dhcp-server, and in that case this + * function should fail with negative errno. */ + + assert(link); + assert(packet); + assert(len > 0); + + if (sendto(s, packet, len, 0, &link->sa, SOCKADDR_LL_LEN(link->ll)) < 0) + return -errno; + + return 0; +} + +int dhcp_network_send_udp_socket( + int s, + be32_t address, + uint16_t port, + const void *packet, + size_t len) { + + union sockaddr_union dest = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(port), + .in.sin_addr.s_addr = address, + }; + + assert(s >= 0); + assert(packet); + assert(len > 0); + + if (sendto(s, packet, len, 0, &dest.sa, sizeof(dest.in)) < 0) + return -errno; + + return 0; +} diff --git a/src/libsystemd-network/dhcp-network.h b/src/libsystemd-network/dhcp-network.h new file mode 100644 index 0000000..eb9dab4 --- /dev/null +++ b/src/libsystemd-network/dhcp-network.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +#include "ether-addr-util.h" +#include "socket-util.h" + +int dhcp_network_bind_raw_socket( + int ifindex, + union sockaddr_union *link, + uint32_t xid, + const struct hw_addr_data *hw_addr, + const struct hw_addr_data *bcast_addr, + uint16_t arp_type, + uint16_t port, + bool so_priority_set, + int so_priority); +int dhcp_network_bind_udp_socket( + int ifindex, + be32_t address, + uint16_t port, + int ip_service_type); +int dhcp_network_send_raw_socket( + int s, + const union sockaddr_union *link, + const void *packet, + size_t len); +int dhcp_network_send_udp_socket( + int s, + be32_t address, + uint16_t port, + const void *packet, + size_t len); diff --git a/src/libsystemd-network/dhcp-option.c b/src/libsystemd-network/dhcp-option.c new file mode 100644 index 0000000..5e216c5 --- /dev/null +++ b/src/libsystemd-network/dhcp-option.c @@ -0,0 +1,461 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <stdint.h> +#include <stdio.h> + +#include "alloc-util.h" +#include "dhcp-option.h" +#include "dhcp-server-internal.h" +#include "memory-util.h" +#include "ordered-set.h" +#include "strv.h" +#include "utf8.h" + +/* Append type-length value structure to the options buffer */ +static int dhcp_option_append_tlv(uint8_t options[], size_t size, size_t *offset, uint8_t code, size_t optlen, const void *optval) { + assert(options); + assert(size > 0); + assert(offset); + assert(optlen <= UINT8_MAX); + assert(*offset < size); + + if (*offset + 2 + optlen > size) + return -ENOBUFS; + + options[*offset] = code; + options[*offset + 1] = optlen; + + memcpy_safe(&options[*offset + 2], optval, optlen); + *offset += 2 + optlen; + return 0; +} + +static int option_append(uint8_t options[], size_t size, size_t *offset, + uint8_t code, size_t optlen, const void *optval) { + assert(options); + assert(size > 0); + assert(offset); + + int r; + + if (code != SD_DHCP_OPTION_END) + /* always make sure there is space for an END option */ + size--; + + switch (code) { + + case SD_DHCP_OPTION_PAD: + case SD_DHCP_OPTION_END: + if (*offset + 1 > size) + return -ENOBUFS; + + options[*offset] = code; + *offset += 1; + break; + + case SD_DHCP_OPTION_USER_CLASS: { + size_t total = 0; + + if (strv_isempty((char **) optval)) + return -EINVAL; + + STRV_FOREACH(s, (const char* const*) optval) { + size_t len = strlen(*s); + + if (len > 255 || len == 0) + return -EINVAL; + + total += 1 + len; + } + + if (*offset + 2 + total > size) + return -ENOBUFS; + + options[*offset] = code; + options[*offset + 1] = total; + *offset += 2; + + STRV_FOREACH(s, (const char* const*) optval) { + size_t len = strlen(*s); + + options[*offset] = len; + memcpy(&options[*offset + 1], *s, len); + *offset += 1 + len; + } + + break; + } + case SD_DHCP_OPTION_SIP_SERVER: + if (*offset + 3 + optlen > size) + return -ENOBUFS; + + options[*offset] = code; + options[*offset + 1] = optlen + 1; + options[*offset + 2] = 1; + + memcpy_safe(&options[*offset + 3], optval, optlen); + *offset += 3 + optlen; + + break; + case SD_DHCP_OPTION_VENDOR_SPECIFIC: { + OrderedSet *s = (OrderedSet *) optval; + struct sd_dhcp_option *p; + size_t l = 0; + + ORDERED_SET_FOREACH(p, s) + l += p->length + 2; + + if (*offset + l + 2 > size) + return -ENOBUFS; + + options[*offset] = code; + options[*offset + 1] = l; + *offset += 2; + + ORDERED_SET_FOREACH(p, s) { + r = dhcp_option_append_tlv(options, size, offset, p->option, p->length, p->data); + if (r < 0) + return r; + } + break; + } + case SD_DHCP_OPTION_RELAY_AGENT_INFORMATION: { + sd_dhcp_server *server = (sd_dhcp_server *) optval; + size_t current_offset = *offset + 2; + + if (server->agent_circuit_id) { + r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_CIRCUIT_ID, + strlen(server->agent_circuit_id), server->agent_circuit_id); + if (r < 0) + return r; + } + if (server->agent_remote_id) { + r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_REMOTE_ID, + strlen(server->agent_remote_id), server->agent_remote_id); + if (r < 0) + return r; + } + + options[*offset] = code; + options[*offset + 1] = current_offset - *offset - 2; + assert(current_offset - *offset - 2 <= UINT8_MAX); + *offset = current_offset; + break; + } + default: + return dhcp_option_append_tlv(options, size, offset, code, optlen, optval); + } + return 0; +} + +static int option_length(uint8_t *options, size_t length, size_t offset) { + assert(options); + assert(offset < length); + + if (IN_SET(options[offset], SD_DHCP_OPTION_PAD, SD_DHCP_OPTION_END)) + return 1; + if (length < offset + 2) + return -ENOBUFS; + + /* validating that buffer is long enough */ + if (length < offset + 2 + options[offset + 1]) + return -ENOBUFS; + + return options[offset + 1] + 2; +} + +int dhcp_option_find_option(uint8_t *options, size_t length, uint8_t code, size_t *ret_offset) { + int r; + + assert(options); + assert(ret_offset); + + for (size_t offset = 0; offset < length; offset += r) { + r = option_length(options, length, offset); + if (r < 0) + return r; + + if (code == options[offset]) { + *ret_offset = offset; + return r; + } + } + return -ENOENT; +} + +int dhcp_option_remove_option(uint8_t *options, size_t length, uint8_t option_code) { + int r; + size_t offset; + + assert(options); + + r = dhcp_option_find_option(options, length, option_code, &offset); + if (r < 0) + return r; + + memmove(options + offset, options + offset + r, length - offset - r); + return length - r; +} + +int dhcp_option_append(DHCPMessage *message, size_t size, size_t *offset, + uint8_t overload, + uint8_t code, size_t optlen, const void *optval) { + const bool use_file = overload & DHCP_OVERLOAD_FILE; + const bool use_sname = overload & DHCP_OVERLOAD_SNAME; + int r; + + assert(message); + assert(offset); + + /* If *offset is in range [0, size), we are writing to ->options, + * if *offset is in range [size, size + sizeof(message->file)) and use_file, we are writing to ->file, + * if *offset is in range [size + use_file*sizeof(message->file), size + use_file*sizeof(message->file) + sizeof(message->sname)) + * and use_sname, we are writing to ->sname. + */ + + if (*offset < size) { + /* still space in the options array */ + r = option_append(message->options, size, offset, code, optlen, optval); + if (r >= 0) + return 0; + else if (r == -ENOBUFS && (use_file || use_sname)) { + /* did not fit, but we have more buffers to try + close the options array and move the offset to its end */ + r = option_append(message->options, size, offset, SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + *offset = size; + } else + return r; + } + + if (use_file) { + size_t file_offset = *offset - size; + + if (file_offset < sizeof(message->file)) { + /* still space in the 'file' array */ + r = option_append(message->file, sizeof(message->file), &file_offset, code, optlen, optval); + if (r >= 0) { + *offset = size + file_offset; + return 0; + } else if (r == -ENOBUFS && use_sname) { + /* did not fit, but we have more buffers to try + close the file array and move the offset to its end */ + r = option_append(message->file, sizeof(message->file), &file_offset, SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + *offset = size + sizeof(message->file); + } else + return r; + } + } + + if (use_sname) { + size_t sname_offset = *offset - size - use_file*sizeof(message->file); + + if (sname_offset < sizeof(message->sname)) { + /* still space in the 'sname' array */ + r = option_append(message->sname, sizeof(message->sname), &sname_offset, code, optlen, optval); + if (r >= 0) { + *offset = size + use_file*sizeof(message->file) + sname_offset; + return 0; + } else + /* no space, or other error, give up */ + return r; + } + } + + return -ENOBUFS; +} + +static int parse_options(const uint8_t options[], size_t buflen, uint8_t *overload, + uint8_t *message_type, char **error_message, dhcp_option_callback_t cb, + void *userdata) { + uint8_t code, len; + const uint8_t *option; + size_t offset = 0; + int r; + + while (offset < buflen) { + code = options[offset ++]; + + switch (code) { + case SD_DHCP_OPTION_PAD: + continue; + + case SD_DHCP_OPTION_END: + return 0; + } + + if (buflen < offset + 1) + return -ENOBUFS; + + len = options[offset ++]; + + if (buflen < offset + len) + return -EINVAL; + + option = &options[offset]; + + switch (code) { + case SD_DHCP_OPTION_MESSAGE_TYPE: + if (len != 1) + return -EINVAL; + + if (message_type) + *message_type = *option; + + break; + + case SD_DHCP_OPTION_ERROR_MESSAGE: + if (len == 0) + return -EINVAL; + + if (error_message) { + _cleanup_free_ char *string = NULL; + + r = make_cstring((const char*) option, len, MAKE_CSTRING_ALLOW_TRAILING_NUL, &string); + if (r < 0) + return r; + + if (!ascii_is_valid(string)) + return -EINVAL; + + free_and_replace(*error_message, string); + } + + break; + case SD_DHCP_OPTION_OVERLOAD: + if (len != 1) + return -EINVAL; + + if (overload) + *overload = *option; + + break; + + default: + if (cb) + cb(code, len, option, userdata); + + break; + } + + offset += len; + } + + if (offset < buflen) + return -EINVAL; + + return 0; +} + +int dhcp_option_parse(DHCPMessage *message, size_t len, dhcp_option_callback_t cb, void *userdata, char **ret_error_message) { + _cleanup_free_ char *error_message = NULL; + uint8_t overload = 0; + uint8_t message_type = 0; + int r; + + if (!message) + return -EINVAL; + + if (len < sizeof(DHCPMessage)) + return -EINVAL; + + len -= sizeof(DHCPMessage); + + r = parse_options(message->options, len, &overload, &message_type, &error_message, cb, userdata); + if (r < 0) + return r; + + if (overload & DHCP_OVERLOAD_FILE) { + r = parse_options(message->file, sizeof(message->file), NULL, &message_type, &error_message, cb, userdata); + if (r < 0) + return r; + } + + if (overload & DHCP_OVERLOAD_SNAME) { + r = parse_options(message->sname, sizeof(message->sname), NULL, &message_type, &error_message, cb, userdata); + if (r < 0) + return r; + } + + if (message_type == 0) + return -ENOMSG; + + if (ret_error_message && IN_SET(message_type, DHCP_NAK, DHCP_DECLINE)) + *ret_error_message = TAKE_PTR(error_message); + + return message_type; +} + +int dhcp_option_parse_string(const uint8_t *option, size_t len, char **ret) { + int r; + + assert(option); + assert(ret); + + if (len <= 0) + *ret = mfree(*ret); + else { + char *string; + + /* + * One trailing NUL byte is OK, we don't mind. See: + * https://github.com/systemd/systemd/issues/1337 + */ + r = make_cstring((const char *) option, len, MAKE_CSTRING_ALLOW_TRAILING_NUL, &string); + if (r < 0) + return r; + + free_and_replace(*ret, string); + } + + return 0; +} + +static sd_dhcp_option* dhcp_option_free(sd_dhcp_option *i) { + if (!i) + return NULL; + + free(i->data); + return mfree(i); +} + +int sd_dhcp_option_new(uint8_t option, const void *data, size_t length, sd_dhcp_option **ret) { + assert_return(ret, -EINVAL); + assert_return(length == 0 || data, -EINVAL); + + _cleanup_free_ void *q = memdup(data, length); + if (!q) + return -ENOMEM; + + sd_dhcp_option *p = new(sd_dhcp_option, 1); + if (!p) + return -ENOMEM; + + *p = (sd_dhcp_option) { + .n_ref = 1, + .option = option, + .length = length, + .data = TAKE_PTR(q), + }; + + *ret = TAKE_PTR(p); + return 0; +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_option, sd_dhcp_option, dhcp_option_free); +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + dhcp_option_hash_ops, + void, + trivial_hash_func, + trivial_compare_func, + sd_dhcp_option, + sd_dhcp_option_unref); diff --git a/src/libsystemd-network/dhcp-option.h b/src/libsystemd-network/dhcp-option.h new file mode 100644 index 0000000..425f5b5 --- /dev/null +++ b/src/libsystemd-network/dhcp-option.h @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdint.h> + +#include "sd-dhcp-option.h" + +#include "dhcp-protocol.h" +#include "hash-funcs.h" + +struct sd_dhcp_option { + unsigned n_ref; + + uint8_t option; + void *data; + size_t length; +}; + +extern const struct hash_ops dhcp_option_hash_ops; + +typedef struct DHCPServerData { + struct in_addr *addr; + size_t size; +} DHCPServerData; + +int dhcp_option_append( + DHCPMessage *message, + size_t size, + size_t *offset, + uint8_t overload, + uint8_t code, + size_t optlen, + const void *optval); +int dhcp_option_find_option(uint8_t *options, size_t length, uint8_t wanted_code, size_t *ret_offset); +int dhcp_option_remove_option(uint8_t *options, size_t buflen, uint8_t option_code); + +typedef int (*dhcp_option_callback_t)(uint8_t code, uint8_t len, const void *option, void *userdata); + +int dhcp_option_parse( + DHCPMessage *message, + size_t len, + dhcp_option_callback_t cb, + void *userdata, + char **ret_error_message); + +int dhcp_option_parse_string(const uint8_t *option, size_t len, char **ret); diff --git a/src/libsystemd-network/dhcp-packet.c b/src/libsystemd-network/dhcp-packet.c new file mode 100644 index 0000000..75b1d7e --- /dev/null +++ b/src/libsystemd-network/dhcp-packet.c @@ -0,0 +1,193 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <net/ethernet.h> +#include <net/if_arp.h> +#include <string.h> + +#include "dhcp-option.h" +#include "dhcp-packet.h" +#include "memory-util.h" + +#define DHCP_CLIENT_MIN_OPTIONS_SIZE 312 + +int dhcp_message_init( + DHCPMessage *message, + uint8_t op, + uint32_t xid, + uint8_t type, + uint16_t arp_type, + uint8_t hlen, + const uint8_t *chaddr, + size_t optlen, + size_t *optoffset) { + + size_t offset = 0; + int r; + + assert(IN_SET(op, BOOTREQUEST, BOOTREPLY)); + assert(chaddr || hlen == 0); + + message->op = op; + message->htype = arp_type; + + /* RFC2131 section 4.1.1: + The client MUST include its hardware address in the ’chaddr’ field, if + necessary for delivery of DHCP reply messages. + + RFC 4390 section 2.1: + A DHCP client, when working over an IPoIB interface, MUST follow the + following rules: + "htype" (hardware address type) MUST be 32 [ARPPARAM]. + "hlen" (hardware address length) MUST be 0. + "chaddr" (client hardware address) field MUST be zeroed. + */ + message->hlen = arp_type == ARPHRD_INFINIBAND ? 0 : hlen; + memcpy_safe(message->chaddr, chaddr, message->hlen); + + message->xid = htobe32(xid); + message->magic = htobe32(DHCP_MAGIC_COOKIE); + + r = dhcp_option_append(message, optlen, &offset, 0, + SD_DHCP_OPTION_MESSAGE_TYPE, 1, &type); + if (r < 0) + return r; + + *optoffset = offset; + + return 0; +} + +uint16_t dhcp_packet_checksum(uint8_t *buf, size_t len) { + uint64_t *buf_64 = (uint64_t*)buf; + uint64_t *end_64 = buf_64 + (len / sizeof(uint64_t)); + uint64_t sum = 0; + + /* See RFC1071 */ + + while (buf_64 < end_64) { + sum += *buf_64; + if (sum < *buf_64) + /* wrap around in one's complement */ + sum++; + + buf_64++; + } + + if (len % sizeof(uint64_t)) { + /* If the buffer is not aligned to 64-bit, we need + to zero-pad the last few bytes and add them in */ + uint64_t buf_tail = 0; + + memcpy(&buf_tail, buf_64, len % sizeof(uint64_t)); + + sum += buf_tail; + if (sum < buf_tail) + /* wrap around */ + sum++; + } + + while (sum >> 16) + sum = (sum & 0xffff) + (sum >> 16); + + return ~sum; +} + +void dhcp_packet_append_ip_headers(DHCPPacket *packet, be32_t source_addr, + uint16_t source_port, be32_t destination_addr, + uint16_t destination_port, uint16_t len, int ip_service_type) { + packet->ip.version = IPVERSION; + packet->ip.ihl = DHCP_IP_SIZE / 4; + packet->ip.tot_len = htobe16(len); + + if (ip_service_type >= 0) + packet->ip.tos = ip_service_type; + else + packet->ip.tos = IPTOS_CLASS_CS6; + + packet->ip.protocol = IPPROTO_UDP; + packet->ip.saddr = source_addr; + packet->ip.daddr = destination_addr; + + packet->udp.source = htobe16(source_port); + packet->udp.dest = htobe16(destination_port); + + packet->udp.len = htobe16(len - DHCP_IP_SIZE); + + packet->ip.check = packet->udp.len; + packet->udp.check = dhcp_packet_checksum((uint8_t*)&packet->ip.ttl, len - 8); + + packet->ip.ttl = IPDEFTTL; + packet->ip.check = 0; + packet->ip.check = dhcp_packet_checksum((uint8_t*)&packet->ip, DHCP_IP_SIZE); +} + +int dhcp_packet_verify_headers(DHCPPacket *packet, size_t len, bool checksum, uint16_t port) { + size_t hdrlen; + + assert(packet); + + if (len < sizeof(DHCPPacket)) + return 0; + + /* IP */ + + if (packet->ip.version != IPVERSION) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: not IPv4"); + + if (packet->ip.ihl < 5) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: IPv4 IHL (%i words) invalid", + packet->ip.ihl); + + hdrlen = packet->ip.ihl * 4; + if (hdrlen < 20) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: IPv4 IHL (%zu bytes) smaller than minimum (20 bytes)", + hdrlen); + + if (len < hdrlen) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: packet (%zu bytes) smaller than expected (%zu) by IP header", + len, hdrlen); + + /* UDP */ + + if (packet->ip.protocol != IPPROTO_UDP) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: not UDP"); + + if (len < hdrlen + be16toh(packet->udp.len)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: packet (%zu bytes) smaller than expected (%zu) by UDP header", + len, hdrlen + be16toh(packet->udp.len)); + + if (be16toh(packet->udp.dest) != port) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: to port %u, which is not the DHCP client port (%u)", + be16toh(packet->udp.dest), port); + + /* checksums - computing these is relatively expensive, so only do it + if all the other checks have passed + */ + + if (dhcp_packet_checksum((uint8_t*)&packet->ip, hdrlen)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: invalid IP checksum"); + + if (checksum && packet->udp.check) { + packet->ip.check = packet->udp.len; + packet->ip.ttl = 0; + + if (dhcp_packet_checksum((uint8_t*)&packet->ip.ttl, + be16toh(packet->udp.len) + 12)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "ignoring packet: invalid UDP checksum"); + } + + return 0; +} diff --git a/src/libsystemd-network/dhcp-packet.h b/src/libsystemd-network/dhcp-packet.h new file mode 100644 index 0000000..751321b --- /dev/null +++ b/src/libsystemd-network/dhcp-packet.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +#include "dhcp-protocol.h" + +int dhcp_message_init( + DHCPMessage *message, + uint8_t op, + uint32_t xid, + uint8_t type, + uint16_t arp_type, + uint8_t hlen, + const uint8_t *chaddr, + size_t optlen, + size_t *optoffset); + +uint16_t dhcp_packet_checksum(uint8_t *buf, size_t len); + +void dhcp_packet_append_ip_headers( + DHCPPacket *packet, + be32_t source_addr, + uint16_t source, + be32_t destination_addr, + uint16_t destination, + uint16_t len, + int ip_service_type); + +int dhcp_packet_verify_headers(DHCPPacket *packet, size_t len, bool checksum, uint16_t port); diff --git a/src/libsystemd-network/dhcp-protocol.h b/src/libsystemd-network/dhcp-protocol.h new file mode 100644 index 0000000..d7bb203 --- /dev/null +++ b/src/libsystemd-network/dhcp-protocol.h @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <netinet/ip.h> +#include <netinet/udp.h> +#include <stdint.h> + +#include "sd-dhcp-protocol.h" + +#include "macro.h" +#include "sparse-endian.h" +#include "time-util.h" + +/* RFC 8925 - IPv6-Only Preferred Option for DHCPv4 3.4. + * MIN_V6ONLY_WAIT: The lower boundary for V6ONLY_WAIT. Value: 300 seconds */ +#define MIN_V6ONLY_WAIT_USEC (300U * USEC_PER_SEC) + +struct DHCPMessage { + uint8_t op; + uint8_t htype; + uint8_t hlen; + uint8_t hops; + be32_t xid; + be16_t secs; + be16_t flags; + be32_t ciaddr; + be32_t yiaddr; + be32_t siaddr; + be32_t giaddr; + uint8_t chaddr[16]; + uint8_t sname[64]; + uint8_t file[128]; + be32_t magic; + uint8_t options[]; +} _packed_; + +typedef struct DHCPMessage DHCPMessage; + +struct DHCPPacket { + struct iphdr ip; + struct udphdr udp; + DHCPMessage dhcp; +} _packed_; + +typedef struct DHCPPacket DHCPPacket; + +#define DHCP_IP_SIZE (int32_t)(sizeof(struct iphdr)) +#define DHCP_IP_UDP_SIZE (int32_t)(sizeof(struct udphdr) + DHCP_IP_SIZE) +#define DHCP_HEADER_SIZE (int32_t)(sizeof(DHCPMessage)) +#define DHCP_MIN_MESSAGE_SIZE 576 /* the minimum internet hosts must be able to receive, see RFC 2132 Section 9.10 */ +#define DHCP_MIN_OPTIONS_SIZE (DHCP_MIN_MESSAGE_SIZE - DHCP_HEADER_SIZE) +#define DHCP_MIN_PACKET_SIZE (DHCP_MIN_MESSAGE_SIZE + DHCP_IP_UDP_SIZE) +#define DHCP_MAGIC_COOKIE (uint32_t)(0x63825363) + +enum { + DHCP_PORT_SERVER = 67, + DHCP_PORT_CLIENT = 68, +}; + +enum { + BOOTREQUEST = 1, + BOOTREPLY = 2, +}; + +enum { + DHCP_DISCOVER = 1, /* [RFC2132] */ + DHCP_OFFER = 2, /* [RFC2132] */ + DHCP_REQUEST = 3, /* [RFC2132] */ + DHCP_DECLINE = 4, /* [RFC2132] */ + DHCP_ACK = 5, /* [RFC2132] */ + DHCP_NAK = 6, /* [RFC2132] */ + DHCP_RELEASE = 7, /* [RFC2132] */ + DHCP_INFORM = 8, /* [RFC2132] */ + DHCP_FORCERENEW = 9, /* [RFC3203] */ + DHCPLEASEQUERY = 10, /* [RFC4388] */ + DHCPLEASEUNASSIGNED = 11, /* [RFC4388] */ + DHCPLEASEUNKNOWN = 12, /* [RFC4388] */ + DHCPLEASEACTIVE = 13, /* [RFC4388] */ + DHCPBULKLEASEQUERY = 14, /* [RFC6926] */ + DHCPLEASEQUERYDONE = 15, /* [RFC6926] */ + DHCPACTIVELEASEQUERY = 16, /* [RFC7724] */ + DHCPLEASEQUERYSTATUS = 17, /* [RFC7724] */ + DHCPTLS = 18, /* [RFC7724] */ +}; + +enum { + DHCP_OVERLOAD_FILE = 1, + DHCP_OVERLOAD_SNAME = 2, +}; + +#define DHCP_MAX_FQDN_LENGTH 255 + +enum { + DHCP_FQDN_FLAG_S = (1 << 0), + DHCP_FQDN_FLAG_O = (1 << 1), + DHCP_FQDN_FLAG_E = (1 << 2), + DHCP_FQDN_FLAG_N = (1 << 3), +}; diff --git a/src/libsystemd-network/dhcp-server-internal.h b/src/libsystemd-network/dhcp-server-internal.h new file mode 100644 index 0000000..da9e56b --- /dev/null +++ b/src/libsystemd-network/dhcp-server-internal.h @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include "sd-dhcp-server.h" +#include "sd-event.h" + +#include "dhcp-option.h" +#include "network-common.h" +#include "ordered-set.h" +#include "time-util.h" + +typedef enum DHCPRawOption { + DHCP_RAW_OPTION_DATA_UINT8, + DHCP_RAW_OPTION_DATA_UINT16, + DHCP_RAW_OPTION_DATA_UINT32, + DHCP_RAW_OPTION_DATA_STRING, + DHCP_RAW_OPTION_DATA_IPV4ADDRESS, + DHCP_RAW_OPTION_DATA_IPV6ADDRESS, + _DHCP_RAW_OPTION_DATA_MAX, + _DHCP_RAW_OPTION_DATA_INVALID, +} DHCPRawOption; + +typedef struct DHCPClientId { + size_t length; + uint8_t *data; +} DHCPClientId; + +typedef struct DHCPLease { + sd_dhcp_server *server; + + DHCPClientId client_id; + + uint8_t htype; /* e.g. ARPHRD_ETHER */ + uint8_t hlen; /* e.g. ETH_ALEN */ + be32_t address; + be32_t gateway; + uint8_t chaddr[16]; + usec_t expiration; + char *hostname; +} DHCPLease; + +struct sd_dhcp_server { + unsigned n_ref; + + sd_event *event; + int event_priority; + sd_event_source *receive_message; + sd_event_source *receive_broadcast; + int fd; + int fd_raw; + int fd_broadcast; + + int ifindex; + char *ifname; + bool bind_to_interface; + be32_t address; + be32_t netmask; + be32_t subnet; + uint32_t pool_offset; + uint32_t pool_size; + + char *timezone; + + DHCPServerData servers[_SD_DHCP_LEASE_SERVER_TYPE_MAX]; + struct in_addr boot_server_address; + char *boot_server_name; + char *boot_filename; + + OrderedSet *extra_options; + OrderedSet *vendor_options; + + bool emit_router; + struct in_addr router_address; + + Hashmap *bound_leases_by_client_id; + Hashmap *bound_leases_by_address; + Hashmap *static_leases_by_client_id; + Hashmap *static_leases_by_address; + + usec_t max_lease_time; + usec_t default_lease_time; + usec_t ipv6_only_preferred_usec; + bool rapid_commit; + + sd_dhcp_server_callback_t callback; + void *callback_userdata; + + struct in_addr relay_target; + + char *agent_circuit_id; + char *agent_remote_id; +}; + +typedef struct DHCPRequest { + /* received message */ + DHCPMessage *message; + + /* options */ + DHCPClientId client_id; + size_t max_optlen; + be32_t server_id; + be32_t requested_ip; + usec_t lifetime; + const uint8_t *agent_info_option; + char *hostname; + const uint8_t *parameter_request_list; + size_t parameter_request_list_len; + bool rapid_commit; + triple_timestamp timestamp; +} DHCPRequest; + +extern const struct hash_ops dhcp_lease_hash_ops; + +int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, + size_t length, const triple_timestamp *timestamp); +int dhcp_server_send_packet(sd_dhcp_server *server, + DHCPRequest *req, DHCPPacket *packet, + int type, size_t optoffset); + +void client_id_hash_func(const DHCPClientId *p, struct siphash *state); +int client_id_compare_func(const DHCPClientId *a, const DHCPClientId *b); + +DHCPLease *dhcp_lease_free(DHCPLease *lease); +DEFINE_TRIVIAL_CLEANUP_FUNC(DHCPLease*, dhcp_lease_free); + +#define log_dhcp_server_errno(server, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "DHCPv4 server: ", \ + sd_dhcp_server, server, \ + error, fmt, ##__VA_ARGS__) +#define log_dhcp_server(server, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "DHCPv4 server: ", \ + sd_dhcp_server, server, \ + 0, fmt, ##__VA_ARGS__) diff --git a/src/libsystemd-network/dhcp6-client-internal.h b/src/libsystemd-network/dhcp6-client-internal.h new file mode 100644 index 0000000..6c17f57 --- /dev/null +++ b/src/libsystemd-network/dhcp6-client-internal.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-dhcp6-client.h" + +int dhcp6_client_set_state_callback( + sd_dhcp6_client *client, + sd_dhcp6_client_callback_t cb, + void *userdata); +int dhcp6_client_get_state(sd_dhcp6_client *client); diff --git a/src/libsystemd-network/dhcp6-internal.h b/src/libsystemd-network/dhcp6-internal.h new file mode 100644 index 0000000..e5b3b13 --- /dev/null +++ b/src/libsystemd-network/dhcp6-internal.h @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014-2015 Intel Corporation. All rights reserved. +***/ + +#include <net/ethernet.h> +#include <netinet/in.h> + +#include "sd-event.h" +#include "sd-dhcp6-client.h" + +#include "dhcp-identifier.h" +#include "dhcp6-client-internal.h" +#include "dhcp6-option.h" +#include "dhcp6-protocol.h" +#include "ether-addr-util.h" +#include "hashmap.h" +#include "macro.h" +#include "network-common.h" +#include "ordered-set.h" +#include "sparse-endian.h" +#include "time-util.h" + +/* what to request from the server, addresses (IA_NA) and/or prefixes (IA_PD) */ +typedef enum DHCP6RequestIA { + DHCP6_REQUEST_IA_NA = 1 << 0, + DHCP6_REQUEST_IA_TA = 1 << 1, /* currently not used */ + DHCP6_REQUEST_IA_PD = 1 << 2, +} DHCP6RequestIA; + +struct sd_dhcp6_client { + unsigned n_ref; + + int ifindex; + char *ifname; + + struct in6_addr local_address; + struct hw_addr_data hw_addr; + uint16_t arp_type; + + sd_event *event; + sd_event_source *receive_message; + sd_event_source *timeout_resend; + sd_event_source *timeout_expire; + sd_event_source *timeout_t1; + sd_event_source *timeout_t2; + int event_priority; + int fd; + + sd_device *dev; + + DHCP6State state; + bool information_request; + usec_t information_request_time_usec; + usec_t information_refresh_time_usec; + be32_t transaction_id; + usec_t transaction_start; + usec_t retransmit_time; + uint8_t retransmit_count; + + bool iaid_set; + DHCP6IA ia_na; + DHCP6IA ia_pd; + DHCP6RequestIA request_ia; + struct duid duid; + size_t duid_len; + be16_t *req_opts; + size_t n_req_opts; + char *fqdn; + char *mudurl; + char **user_class; + char **vendor_class; + OrderedHashmap *extra_options; + OrderedSet *vendor_options; + bool rapid_commit; + + struct sd_dhcp6_lease *lease; + + sd_dhcp6_client_callback_t callback; + void *userdata; + sd_dhcp6_client_callback_t state_callback; + void *state_userdata; + bool send_release; +}; + +int dhcp6_network_bind_udp_socket(int ifindex, struct in6_addr *address); +int dhcp6_network_send_udp_socket(int s, struct in6_addr *address, + const void *packet, size_t len); + +int dhcp6_client_send_message(sd_dhcp6_client *client); +int dhcp6_client_set_transaction_id(sd_dhcp6_client *client, uint32_t transaction_id); + +#define log_dhcp6_client_errno(client, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "DHCPv6 client: ", \ + sd_dhcp6_client, client, \ + error, fmt, ##__VA_ARGS__) +#define log_dhcp6_client(client, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "DHCPv6 client: ", \ + sd_dhcp6_client, client, \ + 0, fmt, ##__VA_ARGS__) diff --git a/src/libsystemd-network/dhcp6-lease-internal.h b/src/libsystemd-network/dhcp6-lease-internal.h new file mode 100644 index 0000000..e76a108 --- /dev/null +++ b/src/libsystemd-network/dhcp6-lease-internal.h @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014-2015 Intel Corporation. All rights reserved. +***/ + +#include <inttypes.h> + +#include "sd-dhcp6-lease.h" + +#include "dhcp6-option.h" +#include "dhcp6-protocol.h" +#include "macro.h" +#include "set.h" +#include "time-util.h" + +struct sd_dhcp6_lease { + unsigned n_ref; + + uint8_t *clientid; + size_t clientid_len; + uint8_t *serverid; + size_t serverid_len; + uint8_t preference; + bool rapid_commit; + triple_timestamp timestamp; + usec_t lifetime_t1; + usec_t lifetime_t2; + usec_t lifetime_valid; + struct in6_addr server_address; + + DHCP6IA *ia_na; /* Identity association non-temporary addresses */ + DHCP6IA *ia_pd; /* Identity association prefix delegation */ + + DHCP6Address *addr_iter; + DHCP6Address *prefix_iter; + + struct in6_addr *dns; + size_t dns_count; + char **domains; + struct in6_addr *ntp; + size_t ntp_count; + char **ntp_fqdn; + struct in6_addr *sntp; + size_t sntp_count; + char *fqdn; + char *captive_portal; + struct sd_dhcp6_option **sorted_vendor_options; + Set *vendor_options; +}; + +int dhcp6_lease_set_clientid(sd_dhcp6_lease *lease, const uint8_t *id, size_t len); +int dhcp6_lease_get_clientid(sd_dhcp6_lease *lease, uint8_t **ret_id, size_t *ret_len); +int dhcp6_lease_set_serverid(sd_dhcp6_lease *lease, const uint8_t *id, size_t len); +int dhcp6_lease_get_serverid(sd_dhcp6_lease *lease, uint8_t **ret_id, size_t *ret_len); +int dhcp6_lease_set_preference(sd_dhcp6_lease *lease, uint8_t preference); +int dhcp6_lease_get_preference(sd_dhcp6_lease *lease, uint8_t *ret); +int dhcp6_lease_set_rapid_commit(sd_dhcp6_lease *lease); +int dhcp6_lease_get_rapid_commit(sd_dhcp6_lease *lease, bool *ret); + +int dhcp6_lease_add_dns(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); +int dhcp6_lease_add_domains(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); +int dhcp6_lease_add_ntp(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); +int dhcp6_lease_add_sntp(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); +int dhcp6_lease_set_fqdn(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); +int dhcp6_lease_set_captive_portal(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); + +int dhcp6_lease_new(sd_dhcp6_lease **ret); +int dhcp6_lease_new_from_message( + sd_dhcp6_client *client, + const DHCP6Message *message, + size_t len, + const triple_timestamp *timestamp, + const struct in6_addr *server_address, + sd_dhcp6_lease **ret); + +#define _FOREACH_DHCP6_ADDRESS(lease, it) \ + for (int it = sd_dhcp6_lease_address_iterator_reset(lease); \ + it > 0; \ + it = sd_dhcp6_lease_address_iterator_next(lease)) +#define FOREACH_DHCP6_ADDRESS(lease) \ + _FOREACH_DHCP6_ADDRESS(lease, UNIQ_T(i, UNIQ)) + +#define _FOREACH_DHCP6_PD_PREFIX(lease, it) \ + for (int it = sd_dhcp6_lease_pd_iterator_reset(lease); \ + it > 0; \ + it = sd_dhcp6_lease_pd_iterator_next(lease)) +#define FOREACH_DHCP6_PD_PREFIX(lease) \ + _FOREACH_DHCP6_PD_PREFIX(lease, UNIQ_T(i, UNIQ)) diff --git a/src/libsystemd-network/dhcp6-network.c b/src/libsystemd-network/dhcp6-network.c new file mode 100644 index 0000000..a3e4e19 --- /dev/null +++ b/src/libsystemd-network/dhcp6-network.c @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <netinet/in.h> +#include <netinet/ip6.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> +#include <linux/if_packet.h> + +#include "dhcp6-internal.h" +#include "dhcp6-protocol.h" +#include "fd-util.h" +#include "socket-util.h" + +int dhcp6_network_bind_udp_socket(int ifindex, struct in6_addr *local_address) { + union sockaddr_union src = { + .in6.sin6_family = AF_INET6, + .in6.sin6_port = htobe16(DHCP6_PORT_CLIENT), + .in6.sin6_scope_id = ifindex, + }; + _cleanup_close_ int s = -EBADF; + int r; + + assert(ifindex > 0); + assert(local_address); + + src.in6.sin6_addr = *local_address; + + s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_UDP); + if (s < 0) + return -errno; + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_V6ONLY, true); + if (r < 0) + return r; + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, false); + if (r < 0) + return r; + + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return r; + + r = setsockopt_int(s, SOL_SOCKET, SO_TIMESTAMP, true); + if (r < 0) + return r; + + r = bind(s, &src.sa, sizeof(src.in6)); + if (r < 0) + return -errno; + + return TAKE_FD(s); +} + +int dhcp6_network_send_udp_socket(int s, struct in6_addr *server_address, + const void *packet, size_t len) { + union sockaddr_union dest = { + .in6.sin6_family = AF_INET6, + .in6.sin6_port = htobe16(DHCP6_PORT_SERVER), + }; + int r; + + assert(server_address); + + memcpy(&dest.in6.sin6_addr, server_address, sizeof(dest.in6.sin6_addr)); + + r = sendto(s, packet, len, 0, &dest.sa, sizeof(dest.in6)); + if (r < 0) + return -errno; + + return 0; +} diff --git a/src/libsystemd-network/dhcp6-option.c b/src/libsystemd-network/dhcp6-option.c new file mode 100644 index 0000000..83f40f3 --- /dev/null +++ b/src/libsystemd-network/dhcp6-option.c @@ -0,0 +1,979 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014-2015 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <netinet/in.h> + +#include "sd-dhcp6-client.h" + +#include "alloc-util.h" +#include "dhcp6-internal.h" +#include "dhcp6-option.h" +#include "dhcp6-protocol.h" +#include "dns-domain.h" +#include "escape.h" +#include "memory-util.h" +#include "network-common.h" +#include "strv.h" +#include "unaligned.h" + +#define DHCP6_OPTION_IA_NA_LEN (sizeof(struct ia_na)) +#define DHCP6_OPTION_IA_PD_LEN (sizeof(struct ia_pd)) +#define DHCP6_OPTION_IA_TA_LEN (sizeof(struct ia_ta)) + +bool dhcp6_option_can_request(uint16_t option) { + /* See Client ORO field in + * https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#dhcpv6-parameters-2 */ + + switch (option) { + case SD_DHCP6_OPTION_CLIENTID: + case SD_DHCP6_OPTION_SERVERID: + case SD_DHCP6_OPTION_IA_NA: + case SD_DHCP6_OPTION_IA_TA: + case SD_DHCP6_OPTION_IAADDR: + case SD_DHCP6_OPTION_ORO: + case SD_DHCP6_OPTION_PREFERENCE: + case SD_DHCP6_OPTION_ELAPSED_TIME: + case SD_DHCP6_OPTION_RELAY_MSG: + case SD_DHCP6_OPTION_AUTH: + case SD_DHCP6_OPTION_UNICAST: + case SD_DHCP6_OPTION_STATUS_CODE: + case SD_DHCP6_OPTION_RAPID_COMMIT: + case SD_DHCP6_OPTION_USER_CLASS: + case SD_DHCP6_OPTION_VENDOR_CLASS: + return false; + case SD_DHCP6_OPTION_VENDOR_OPTS: + return true; + case SD_DHCP6_OPTION_INTERFACE_ID: + case SD_DHCP6_OPTION_RECONF_MSG: + case SD_DHCP6_OPTION_RECONF_ACCEPT: + return false; + case SD_DHCP6_OPTION_SIP_SERVER_DOMAIN_NAME: + case SD_DHCP6_OPTION_SIP_SERVER_ADDRESS: + case SD_DHCP6_OPTION_DNS_SERVER: + case SD_DHCP6_OPTION_DOMAIN: + return true; + case SD_DHCP6_OPTION_IA_PD: + case SD_DHCP6_OPTION_IA_PD_PREFIX: + return false; + case SD_DHCP6_OPTION_NIS_SERVER: + case SD_DHCP6_OPTION_NISP_SERVER: + case SD_DHCP6_OPTION_NIS_DOMAIN_NAME: + case SD_DHCP6_OPTION_NISP_DOMAIN_NAME: + case SD_DHCP6_OPTION_SNTP_SERVER: + return true; + case SD_DHCP6_OPTION_INFORMATION_REFRESH_TIME: + return false; /* This is automatically set when sending INFORMATION_REQUEST message. */ + case SD_DHCP6_OPTION_BCMCS_SERVER_D: + case SD_DHCP6_OPTION_BCMCS_SERVER_A: + case SD_DHCP6_OPTION_GEOCONF_CIVIC: + return true; + case SD_DHCP6_OPTION_REMOTE_ID: + case SD_DHCP6_OPTION_SUBSCRIBER_ID: + return false; + case SD_DHCP6_OPTION_CLIENT_FQDN: + case SD_DHCP6_OPTION_PANA_AGENT: + case SD_DHCP6_OPTION_POSIX_TIMEZONE: + case SD_DHCP6_OPTION_TZDB_TIMEZONE: + return true; + case SD_DHCP6_OPTION_ERO: + case SD_DHCP6_OPTION_LQ_QUERY: + case SD_DHCP6_OPTION_CLIENT_DATA: + case SD_DHCP6_OPTION_CLT_TIME: + case SD_DHCP6_OPTION_LQ_RELAY_DATA: + case SD_DHCP6_OPTION_LQ_CLIENT_LINK: + return false; + case SD_DHCP6_OPTION_MIP6_HNIDF: + case SD_DHCP6_OPTION_MIP6_VDINF: + case SD_DHCP6_OPTION_V6_LOST: + case SD_DHCP6_OPTION_CAPWAP_AC_V6: + return true; + case SD_DHCP6_OPTION_RELAY_ID: + return false; + case SD_DHCP6_OPTION_IPV6_ADDRESS_MOS: + case SD_DHCP6_OPTION_IPV6_FQDN_MOS: + case SD_DHCP6_OPTION_NTP_SERVER: + case SD_DHCP6_OPTION_V6_ACCESS_DOMAIN: + case SD_DHCP6_OPTION_SIP_UA_CS_LIST: + case SD_DHCP6_OPTION_BOOTFILE_URL: + case SD_DHCP6_OPTION_BOOTFILE_PARAM: + return true; + case SD_DHCP6_OPTION_CLIENT_ARCH_TYPE: + return false; + case SD_DHCP6_OPTION_NII: + case SD_DHCP6_OPTION_GEOLOCATION: + case SD_DHCP6_OPTION_AFTR_NAME: + case SD_DHCP6_OPTION_ERP_LOCAL_DOMAIN_NAME: + return true; + case SD_DHCP6_OPTION_RSOO: + return false; + case SD_DHCP6_OPTION_PD_EXCLUDE: + return true; + case SD_DHCP6_OPTION_VSS: + return false; + case SD_DHCP6_OPTION_MIP6_IDINF: + case SD_DHCP6_OPTION_MIP6_UDINF: + case SD_DHCP6_OPTION_MIP6_HNP: + case SD_DHCP6_OPTION_MIP6_HAA: + case SD_DHCP6_OPTION_MIP6_HAF: + case SD_DHCP6_OPTION_RDNSS_SELECTION: + case SD_DHCP6_OPTION_KRB_PRINCIPAL_NAME: + case SD_DHCP6_OPTION_KRB_REALM_NAME: + case SD_DHCP6_OPTION_KRB_DEFAULT_REALM_NAME: + case SD_DHCP6_OPTION_KRB_KDC: + return true; + case SD_DHCP6_OPTION_CLIENT_LINKLAYER_ADDR: + case SD_DHCP6_OPTION_LINK_ADDRESS: + case SD_DHCP6_OPTION_RADIUS: + case SD_DHCP6_OPTION_SOL_MAX_RT: /* Automatically set when sending SOLICIT message. */ + case SD_DHCP6_OPTION_INF_MAX_RT: /* Automatically set when sending INFORMATION_REQUEST message. */ + return false; + case SD_DHCP6_OPTION_ADDRSEL: + case SD_DHCP6_OPTION_ADDRSEL_TABLE: + case SD_DHCP6_OPTION_V6_PCP_SERVER: + return true; + case SD_DHCP6_OPTION_DHCPV4_MSG: + return false; + case SD_DHCP6_OPTION_DHCP4_O_DHCP6_SERVER: + return true; + case SD_DHCP6_OPTION_S46_RULE: + return false; + case SD_DHCP6_OPTION_S46_BR: + return true; + case SD_DHCP6_OPTION_S46_DMR: + case SD_DHCP6_OPTION_S46_V4V6BIND: + case SD_DHCP6_OPTION_S46_PORTPARAMS: + return false; + case SD_DHCP6_OPTION_S46_CONT_MAPE: + case SD_DHCP6_OPTION_S46_CONT_MAPT: + case SD_DHCP6_OPTION_S46_CONT_LW: + case SD_DHCP6_OPTION_4RD: + case SD_DHCP6_OPTION_4RD_MAP_RULE: + case SD_DHCP6_OPTION_4RD_NON_MAP_RULE: + return true; + case SD_DHCP6_OPTION_LQ_BASE_TIME: + case SD_DHCP6_OPTION_LQ_START_TIME: + case SD_DHCP6_OPTION_LQ_END_TIME: + return false; + case SD_DHCP6_OPTION_CAPTIVE_PORTAL: + case SD_DHCP6_OPTION_MPL_PARAMETERS: + return true; + case SD_DHCP6_OPTION_ANI_ATT: + case SD_DHCP6_OPTION_ANI_NETWORK_NAME: + case SD_DHCP6_OPTION_ANI_AP_NAME: + case SD_DHCP6_OPTION_ANI_AP_BSSID: + case SD_DHCP6_OPTION_ANI_OPERATOR_ID: + case SD_DHCP6_OPTION_ANI_OPERATOR_REALM: + return false; + case SD_DHCP6_OPTION_S46_PRIORITY: + return true; + case SD_DHCP6_OPTION_MUD_URL_V6: + return false; + case SD_DHCP6_OPTION_V6_PREFIX64: + return true; + case SD_DHCP6_OPTION_F_BINDING_STATUS: + case SD_DHCP6_OPTION_F_CONNECT_FLAGS: + case SD_DHCP6_OPTION_F_DNS_REMOVAL_INFO: + case SD_DHCP6_OPTION_F_DNS_HOST_NAME: + case SD_DHCP6_OPTION_F_DNS_ZONE_NAME: + case SD_DHCP6_OPTION_F_DNS_FLAGS: + case SD_DHCP6_OPTION_F_EXPIRATION_TIME: + case SD_DHCP6_OPTION_F_MAX_UNACKED_BNDUPD: + case SD_DHCP6_OPTION_F_MCLT: + case SD_DHCP6_OPTION_F_PARTNER_LIFETIME: + case SD_DHCP6_OPTION_F_PARTNER_LIFETIME_SENT: + case SD_DHCP6_OPTION_F_PARTNER_DOWN_TIME: + case SD_DHCP6_OPTION_F_PARTNER_RAW_CLT_TIME: + case SD_DHCP6_OPTION_F_PROTOCOL_VERSION: + case SD_DHCP6_OPTION_F_KEEPALIVE_TIME: + case SD_DHCP6_OPTION_F_RECONFIGURE_DATA: + case SD_DHCP6_OPTION_F_RELATIONSHIP_NAME: + case SD_DHCP6_OPTION_F_SERVER_FLAGS: + case SD_DHCP6_OPTION_F_SERVER_STATE: + case SD_DHCP6_OPTION_F_START_TIME_OF_STATE: + case SD_DHCP6_OPTION_F_STATE_EXPIRATION_TIME: + case SD_DHCP6_OPTION_RELAY_PORT: + return false; + case SD_DHCP6_OPTION_V6_SZTP_REDIRECT: + case SD_DHCP6_OPTION_S46_BIND_IPV6_PREFIX: + return true; + case SD_DHCP6_OPTION_IA_LL: + case SD_DHCP6_OPTION_LLADDR: + case SD_DHCP6_OPTION_SLAP_QUAD: + return false; + case SD_DHCP6_OPTION_V6_DOTS_RI: + case SD_DHCP6_OPTION_V6_DOTS_ADDRESS: + case SD_DHCP6_OPTION_IPV6_ADDRESS_ANDSF: + return true; + default: + return false; + } +} + +static int option_append_hdr(uint8_t **buf, size_t *offset, uint16_t optcode, size_t optlen) { + assert(buf); + assert(*buf); + assert(offset); + + if (optlen > 0xffff) + return -ENOBUFS; + + if (optlen + offsetof(DHCP6Option, data) > SIZE_MAX - *offset) + return -ENOBUFS; + + if (!GREEDY_REALLOC(*buf, *offset + optlen + offsetof(DHCP6Option, data))) + return -ENOMEM; + + unaligned_write_be16(*buf + *offset + offsetof(DHCP6Option, code), optcode); + unaligned_write_be16(*buf + *offset + offsetof(DHCP6Option, len), optlen); + + *offset += offsetof(DHCP6Option, data); + return 0; +} + +int dhcp6_option_append( + uint8_t **buf, + size_t *offset, + uint16_t code, + size_t optlen, + const void *optval) { + + int r; + + assert(optval || optlen == 0); + + r = option_append_hdr(buf, offset, code, optlen); + if (r < 0) + return r; + + memcpy_safe(*buf + *offset, optval, optlen); + *offset += optlen; + + return 0; +} + +int dhcp6_option_append_vendor_option(uint8_t **buf, size_t *offset, OrderedSet *vendor_options) { + sd_dhcp6_option *options; + int r; + + assert(buf); + assert(*buf); + assert(offset); + + ORDERED_SET_FOREACH(options, vendor_options) { + _cleanup_free_ uint8_t *p = NULL; + size_t total; + + total = 4 + 2 + 2 + options->length; + + p = malloc(total); + if (!p) + return -ENOMEM; + + unaligned_write_be32(p, options->enterprise_identifier); + unaligned_write_be16(p + 4, options->option); + unaligned_write_be16(p + 6, options->length); + memcpy(p + 8, options->data, options->length); + + r = dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_VENDOR_OPTS, total, p); + if (r < 0) + return r; + } + + return 0; +} + +static int option_append_ia_address(uint8_t **buf, size_t *offset, const struct iaaddr *address) { + assert(buf); + assert(*buf); + assert(offset); + assert(address); + + /* Do not append T1 and T2. */ + const struct iaaddr a = { + .address = address->address, + }; + + return dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_IAADDR, sizeof(struct iaaddr), &a); +} + +static int option_append_pd_prefix(uint8_t **buf, size_t *offset, const struct iapdprefix *prefix) { + assert(buf); + assert(*buf); + assert(offset); + assert(prefix); + + if (prefix->prefixlen == 0) + return -EINVAL; + + /* Do not append T1 and T2. */ + const struct iapdprefix p = { + .prefixlen = prefix->prefixlen, + .address = prefix->address, + }; + + return dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_IA_PD_PREFIX, sizeof(struct iapdprefix), &p); +} + +int dhcp6_option_append_ia(uint8_t **buf, size_t *offset, const DHCP6IA *ia) { + _cleanup_free_ uint8_t *data = NULL; + struct ia_header header; + size_t len; + int r; + + assert(buf); + assert(*buf); + assert(offset); + assert(ia); + + /* client should not send set T1 and T2. See, RFC 8415, and issue #18090. */ + + switch (ia->type) { + case SD_DHCP6_OPTION_IA_NA: + case SD_DHCP6_OPTION_IA_PD: + len = sizeof(struct ia_header); + header = (struct ia_header) { + .id = ia->header.id, + }; + break; + + case SD_DHCP6_OPTION_IA_TA: + len = sizeof(header.id); /* IA_TA does not have lifetime. */ + header = (struct ia_header) { + .id = ia->header.id, + }; + break; + + default: + assert_not_reached(); + } + + if (!GREEDY_REALLOC(data, len)) + return -ENOMEM; + + memcpy(data, &header, len); + + LIST_FOREACH(addresses, addr, ia->addresses) { + if (ia->type == SD_DHCP6_OPTION_IA_PD) + r = option_append_pd_prefix(&data, &len, &addr->iapdprefix); + else + r = option_append_ia_address(&data, &len, &addr->iaaddr); + if (r < 0) + return r; + } + + return dhcp6_option_append(buf, offset, ia->type, len, data); +} + +int dhcp6_option_append_fqdn(uint8_t **buf, size_t *offset, const char *fqdn) { + uint8_t buffer[1 + DNS_WIRE_FORMAT_HOSTNAME_MAX]; + int r; + + assert(buf); + assert(*buf); + assert(offset); + + if (isempty(fqdn)) + return 0; + + buffer[0] = DHCP6_FQDN_FLAG_S; /* Request server to perform AAAA RR DNS updates */ + + /* Store domain name after flags field */ + r = dns_name_to_wire_format(fqdn, buffer + 1, sizeof(buffer) - 1, false); + if (r <= 0) + return r; + + /* + * According to RFC 4704, chapter 4.2 only add terminating zero-length + * label in case a FQDN is provided. Since dns_name_to_wire_format + * always adds terminating zero-length label remove if only a hostname + * is provided. + */ + if (dns_name_is_single_label(fqdn)) + r--; + + return dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_CLIENT_FQDN, 1 + r, buffer); +} + +int dhcp6_option_append_user_class(uint8_t **buf, size_t *offset, char * const *user_class) { + _cleanup_free_ uint8_t *p = NULL; + size_t n = 0; + + assert(buf); + assert(*buf); + assert(offset); + + if (strv_isempty(user_class)) + return 0; + + STRV_FOREACH(s, user_class) { + size_t len = strlen(*s); + + if (len > UINT16_MAX || len == 0) + return -EINVAL; + + if (!GREEDY_REALLOC(p, n + len + 2)) + return -ENOMEM; + + unaligned_write_be16(p + n, len); + memcpy(p + n + 2, *s, len); + n += len + 2; + } + + return dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_USER_CLASS, n, p); +} + +int dhcp6_option_append_vendor_class(uint8_t **buf, size_t *offset, char * const *vendor_class) { + _cleanup_free_ uint8_t *p = NULL; + size_t n = 0; + + assert(buf); + assert(*buf); + assert(offset); + + if (strv_isempty(vendor_class)) + return 0; + + if (!GREEDY_REALLOC(p, sizeof(be32_t))) + return -ENOMEM; + + /* Enterprise Identifier */ + unaligned_write_be32(p, SYSTEMD_PEN); + n += sizeof(be32_t); + + STRV_FOREACH(s, vendor_class) { + size_t len = strlen(*s); + + if (len > UINT16_MAX || len == 0) + return -EINVAL; + + if (!GREEDY_REALLOC(p, n + len + 2)) + return -ENOMEM; + + unaligned_write_be16(p + n, len); + memcpy(p + n + 2, *s, len); + n += len + 2; + } + + return dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_VENDOR_CLASS, n, p); +} + +int dhcp6_option_parse( + const uint8_t *buf, + size_t buflen, + size_t *offset, + uint16_t *ret_option_code, + size_t *ret_option_data_len, + const uint8_t **ret_option_data) { + + size_t len; + + assert(buf); + assert(offset); + assert(ret_option_code); + assert(ret_option_data_len); + assert(ret_option_data); + + if (buflen < offsetof(DHCP6Option, data)) + return -EBADMSG; + + if (*offset > buflen - offsetof(DHCP6Option, data)) + return -EBADMSG; + + len = unaligned_read_be16(buf + *offset + offsetof(DHCP6Option, len)); + + if (len > buflen - offsetof(DHCP6Option, data) - *offset) + return -EBADMSG; + + *ret_option_code = unaligned_read_be16(buf + *offset + offsetof(DHCP6Option, code)); + *ret_option_data_len = len; + *ret_option_data = len == 0 ? NULL : buf + *offset + offsetof(DHCP6Option, data); + *offset += offsetof(DHCP6Option, data) + len; + + return 0; +} + +int dhcp6_option_parse_status(const uint8_t *data, size_t data_len, char **ret_status_message) { + DHCP6Status status; + + assert(data || data_len == 0); + + if (data_len < sizeof(uint16_t)) + return -EBADMSG; + + status = unaligned_read_be16(data); + + if (ret_status_message) { + _cleanup_free_ char *msg = NULL; + const char *s; + + /* The status message MUST NOT be null-terminated. See section 21.13 of RFC8415. + * Let's escape unsafe characters for safety. */ + msg = cescape_length((const char*) (data + sizeof(uint16_t)), data_len - sizeof(uint16_t)); + if (!msg) + return -ENOMEM; + + s = dhcp6_message_status_to_string(status); + if (s && !strextend_with_separator(&msg, ": ", s)) + return -ENOMEM; + + *ret_status_message = TAKE_PTR(msg); + } + + return status; +} + +/* parse a string from dhcp option field. *ret must be initialized */ +int dhcp6_option_parse_string(const uint8_t *data, size_t data_len, char **ret) { + _cleanup_free_ char *string = NULL; + int r; + + assert(data || data_len == 0); + assert(ret); + + if (data_len <= 0) { + *ret = mfree(*ret); + return 0; + } + + r = make_cstring((const char *) data, data_len, MAKE_CSTRING_REFUSE_TRAILING_NUL, &string); + if (r < 0) + return r; + + return free_and_replace(*ret, string); +} + +static int dhcp6_option_parse_ia_options(sd_dhcp6_client *client, const uint8_t *buf, size_t buflen) { + int r; + + assert(buf || buflen == 0); + + for (size_t offset = 0; offset < buflen;) { + const uint8_t *data; + size_t data_len; + uint16_t code; + + r = dhcp6_option_parse(buf, buflen, &offset, &code, &data_len, &data); + if (r < 0) + return r; + + switch (code) { + case SD_DHCP6_OPTION_STATUS_CODE: { + _cleanup_free_ char *msg = NULL; + + r = dhcp6_option_parse_status(data, data_len, &msg); + if (r == -ENOMEM) + return r; + if (r > 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an IA address or PD prefix option with non-zero status%s%s", + isempty(msg) ? "." : ": ", strempty(msg)); + if (r < 0) + /* Let's log but ignore the invalid status option. */ + log_dhcp6_client_errno(client, r, + "Received an IA address or PD prefix option with an invalid status sub option, ignoring: %m"); + break; + } + default: + log_dhcp6_client(client, "Received an unknown sub option %u in IA address or PD prefix, ignoring.", code); + } + } + + return 0; +} + +static int dhcp6_option_parse_ia_address(sd_dhcp6_client *client, DHCP6IA *ia, const uint8_t *data, size_t len) { + _cleanup_free_ DHCP6Address *a = NULL; + usec_t lt_valid, lt_pref; + int r; + + assert(ia); + assert(data || len == 0); + + if (!IN_SET(ia->type, SD_DHCP6_OPTION_IA_NA, SD_DHCP6_OPTION_IA_TA)) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an IA address sub-option in an invalid option, ignoring."); + + if (len < sizeof(struct iaaddr)) + return -EBADMSG; + + a = new(DHCP6Address, 1); + if (!a) + return -ENOMEM; + + memcpy(&a->iaaddr, data, sizeof(struct iaaddr)); + + lt_valid = be32_sec_to_usec(a->iaaddr.lifetime_valid, /* max_as_infinity = */ true); + lt_pref = be32_sec_to_usec(a->iaaddr.lifetime_preferred, /* max_as_infinity = */ true); + + if (lt_valid == 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an IA address with zero valid lifetime, ignoring."); + if (lt_pref > lt_valid) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an IA address with preferred lifetime %s " + "larger than valid lifetime %s, ignoring.", + FORMAT_TIMESPAN(lt_pref, USEC_PER_SEC), + FORMAT_TIMESPAN(lt_valid, USEC_PER_SEC)); + + if (len > sizeof(struct iaaddr)) { + r = dhcp6_option_parse_ia_options(client, data + sizeof(struct iaaddr), len - sizeof(struct iaaddr)); + if (r < 0) + return r; + } + + LIST_PREPEND(addresses, ia->addresses, TAKE_PTR(a)); + return 0; +} + +static int dhcp6_option_parse_ia_pdprefix(sd_dhcp6_client *client, DHCP6IA *ia, const uint8_t *data, size_t len) { + _cleanup_free_ DHCP6Address *a = NULL; + usec_t lt_valid, lt_pref; + int r; + + assert(ia); + assert(data || len == 0); + + if (ia->type != SD_DHCP6_OPTION_IA_PD) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an PD prefix sub-option in an invalid option, ignoring"); + + if (len < sizeof(struct iapdprefix)) + return -EBADMSG; + + a = new(DHCP6Address, 1); + if (!a) + return -ENOMEM; + + memcpy(&a->iapdprefix, data, sizeof(struct iapdprefix)); + + lt_valid = be32_sec_to_usec(a->iapdprefix.lifetime_valid, /* max_as_infinity = */ true); + lt_pref = be32_sec_to_usec(a->iapdprefix.lifetime_preferred, /* max_as_infinity = */ true); + + if (lt_valid == 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received a PD prefix with zero valid lifetime, ignoring."); + if (lt_pref > lt_valid) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received a PD prefix with preferred lifetime %s " + "larger than valid lifetime %s, ignoring.", + FORMAT_TIMESPAN(lt_pref, USEC_PER_SEC), + FORMAT_TIMESPAN(lt_valid, USEC_PER_SEC)); + + if (len > sizeof(struct iapdprefix)) { + r = dhcp6_option_parse_ia_options(client, data + sizeof(struct iapdprefix), len - sizeof(struct iapdprefix)); + if (r < 0) + return r; + } + + LIST_PREPEND(addresses, ia->addresses, TAKE_PTR(a)); + return 0; +} + +int dhcp6_option_parse_ia( + sd_dhcp6_client *client, + be32_t iaid, + uint16_t option_code, + size_t option_data_len, + const uint8_t *option_data, + DHCP6IA **ret) { + + _cleanup_(dhcp6_ia_freep) DHCP6IA *ia = NULL; + usec_t lt_t1, lt_t2; + size_t header_len; + int r; + + assert(IN_SET(option_code, SD_DHCP6_OPTION_IA_NA, SD_DHCP6_OPTION_IA_TA, SD_DHCP6_OPTION_IA_PD)); + assert(option_data || option_data_len == 0); + assert(ret); + + /* This will return the following: + * -ENOMEM: memory allocation error, + * -ENOANO: unmatching IAID, + * -EINVAL: non-zero status code, or invalid lifetime, + * -EBADMSG: invalid message format, + * -ENODATA: no valid address or PD prefix, + * 0: success. */ + + switch (option_code) { + case SD_DHCP6_OPTION_IA_NA: + case SD_DHCP6_OPTION_IA_PD: + header_len = sizeof(struct ia_header); + break; + + case SD_DHCP6_OPTION_IA_TA: + header_len = sizeof(be32_t); /* IA_TA does not have lifetime. */ + break; + + default: + assert_not_reached(); + } + + if (option_data_len < header_len) + return -EBADMSG; + + ia = new(DHCP6IA, 1); + if (!ia) + return -ENOMEM; + + *ia = (DHCP6IA) { + .type = option_code, + }; + memcpy(&ia->header, option_data, header_len); + + /* According to RFC8415, IAs which do not match the client's IAID should be ignored, + * but not necessary to ignore or refuse the whole message. */ + if (ia->header.id != iaid) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(ENOANO), + "Received an IA option with a different IAID " + "from the one chosen by the client, ignoring."); + + /* It is not necessary to check if the lifetime_t2 is zero here, as in that case it will be updated later. */ + lt_t1 = be32_sec_to_usec(ia->header.lifetime_t1, /* max_as_infinity = */ true); + lt_t2 = be32_sec_to_usec(ia->header.lifetime_t2, /* max_as_infinity = */ true); + + if (lt_t1 > lt_t2) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an IA option with T1 %s > T2 %s, ignoring.", + FORMAT_TIMESPAN(lt_t1, USEC_PER_SEC), + FORMAT_TIMESPAN(lt_t2, USEC_PER_SEC)); + if (lt_t1 == 0 && lt_t2 > 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an IA option with zero T1 and non-zero T2 (%s), ignoring.", + FORMAT_TIMESPAN(lt_t2, USEC_PER_SEC)); + + for (size_t offset = header_len; offset < option_data_len;) { + const uint8_t *subdata; + size_t subdata_len; + uint16_t subopt; + + r = dhcp6_option_parse(option_data, option_data_len, &offset, &subopt, &subdata_len, &subdata); + if (r < 0) + return r; + + switch (subopt) { + case SD_DHCP6_OPTION_IAADDR: { + r = dhcp6_option_parse_ia_address(client, ia, subdata, subdata_len); + if (r == -ENOMEM) + return r; + + /* Ignore non-critical errors in the sub-option. */ + break; + } + case SD_DHCP6_OPTION_IA_PD_PREFIX: { + r = dhcp6_option_parse_ia_pdprefix(client, ia, subdata, subdata_len); + if (r == -ENOMEM) + return r; + + /* Ignore non-critical errors in the sub-option. */ + break; + } + case SD_DHCP6_OPTION_STATUS_CODE: { + _cleanup_free_ char *msg = NULL; + + r = dhcp6_option_parse_status(subdata, subdata_len, &msg); + if (r == -ENOMEM) + return r; + if (r > 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received an IA option with non-zero status%s%s", + isempty(msg) ? "." : ": ", strempty(msg)); + if (r < 0) + log_dhcp6_client_errno(client, r, + "Received an IA option with an invalid status sub option, ignoring: %m"); + break; + } + default: + log_dhcp6_client(client, "Received an IA option with an unknown sub-option %u, ignoring", subopt); + } + } + + if (!ia->addresses) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(ENODATA), + "Received an IA option without valid IA addresses or PD prefixes, ignoring."); + + *ret = TAKE_PTR(ia); + return 0; +} + +int dhcp6_option_parse_addresses( + const uint8_t *optval, + size_t optlen, + struct in6_addr **addrs, + size_t *count) { + + assert(optval || optlen == 0); + assert(addrs); + assert(count); + + if (optlen == 0 || optlen % sizeof(struct in6_addr) != 0) + return -EBADMSG; + + if (!GREEDY_REALLOC(*addrs, *count + optlen / sizeof(struct in6_addr))) + return -ENOMEM; + + memcpy(*addrs + *count, optval, optlen); + *count += optlen / sizeof(struct in6_addr); + + return 0; +} + +static int parse_domain(const uint8_t **data, size_t *len, char **ret) { + _cleanup_free_ char *domain = NULL; + const uint8_t *optval; + size_t optlen, n = 0; + int r; + + assert(data); + assert(len); + assert(*data || *len == 0); + assert(ret); + + optval = *data; + optlen = *len; + + if (optlen <= 1) + return -ENODATA; + + for (;;) { + const char *label; + uint8_t c; + + if (optlen == 0) + break; + + c = *optval; + optval++; + optlen--; + + if (c == 0) + /* End label */ + break; + if (c > 63) + return -EBADMSG; + if (c > optlen) + return -EMSGSIZE; + + /* Literal label */ + label = (const char*) optval; + optval += c; + optlen -= c; + + if (!GREEDY_REALLOC(domain, n + (n != 0) + DNS_LABEL_ESCAPED_MAX)) + return -ENOMEM; + + if (n != 0) + domain[n++] = '.'; + + r = dns_label_escape(label, c, domain + n, DNS_LABEL_ESCAPED_MAX); + if (r < 0) + return r; + + n += r; + } + + if (n > 0) { + if (!GREEDY_REALLOC(domain, n + 1)) + return -ENOMEM; + + domain[n] = '\0'; + } + + *ret = TAKE_PTR(domain); + *data = optval; + *len = optlen; + + return n; +} + +int dhcp6_option_parse_domainname(const uint8_t *optval, size_t optlen, char **ret) { + _cleanup_free_ char *domain = NULL; + int r; + + assert(optval || optlen == 0); + assert(ret); + + r = parse_domain(&optval, &optlen, &domain); + if (r < 0) + return r; + if (r == 0) + return -ENODATA; + if (optlen != 0) + return -EINVAL; + + *ret = TAKE_PTR(domain); + return 0; +} + +int dhcp6_option_parse_domainname_list(const uint8_t *optval, size_t optlen, char ***ret) { + _cleanup_strv_free_ char **names = NULL; + int r; + + assert(optval || optlen == 0); + assert(ret); + + if (optlen <= 1) + return -ENODATA; + if (optval[optlen - 1] != '\0') + return -EINVAL; + + while (optlen > 0) { + _cleanup_free_ char *name = NULL; + + r = parse_domain(&optval, &optlen, &name); + if (r < 0) + return r; + if (r == 0) + continue; + + r = strv_consume(&names, TAKE_PTR(name)); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(names); + return 0; +} + +static sd_dhcp6_option* dhcp6_option_free(sd_dhcp6_option *i) { + if (!i) + return NULL; + + free(i->data); + return mfree(i); +} + +int sd_dhcp6_option_new(uint16_t option, const void *data, size_t length, uint32_t enterprise_identifier, sd_dhcp6_option **ret) { + assert_return(ret, -EINVAL); + assert_return(length == 0 || data, -EINVAL); + + _cleanup_free_ void *q = memdup(data, length); + if (!q) + return -ENOMEM; + + sd_dhcp6_option *p = new(sd_dhcp6_option, 1); + if (!p) + return -ENOMEM; + + *p = (sd_dhcp6_option) { + .n_ref = 1, + .option = option, + .enterprise_identifier = enterprise_identifier, + .length = length, + .data = TAKE_PTR(q), + }; + + *ret = p; + return 0; +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp6_option, sd_dhcp6_option, dhcp6_option_free); +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + dhcp6_option_hash_ops, + void, + trivial_hash_func, + trivial_compare_func, + sd_dhcp6_option, + sd_dhcp6_option_unref); diff --git a/src/libsystemd-network/dhcp6-option.h b/src/libsystemd-network/dhcp6-option.h new file mode 100644 index 0000000..614b4f8 --- /dev/null +++ b/src/libsystemd-network/dhcp6-option.h @@ -0,0 +1,105 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-dhcp6-client.h" + +#include "hash-funcs.h" +#include "list.h" +#include "macro.h" +#include "ordered-set.h" +#include "sparse-endian.h" + +typedef struct sd_dhcp6_option { + unsigned n_ref; + + uint32_t enterprise_identifier; + uint16_t option; + void *data; + size_t length; +} sd_dhcp6_option; + +extern const struct hash_ops dhcp6_option_hash_ops; + +/* Common option header */ +typedef struct DHCP6Option { + be16_t code; + be16_t len; + uint8_t data[]; +} _packed_ DHCP6Option; + +/* Address option */ +struct iaaddr { + struct in6_addr address; + be32_t lifetime_preferred; + be32_t lifetime_valid; +} _packed_; + +/* Prefix Delegation Prefix option */ +struct iapdprefix { + be32_t lifetime_preferred; + be32_t lifetime_valid; + uint8_t prefixlen; + struct in6_addr address; +} _packed_; + +typedef struct DHCP6Address DHCP6Address; + +struct DHCP6Address { + LIST_FIELDS(DHCP6Address, addresses); + + union { + struct iaaddr iaaddr; + struct iapdprefix iapdprefix; + }; +}; + +struct ia_header { + be32_t id; + be32_t lifetime_t1; + be32_t lifetime_t2; +} _packed_; + +typedef struct DHCP6IA { + uint16_t type; + struct ia_header header; + + LIST_HEAD(DHCP6Address, addresses); +} DHCP6IA; + +void dhcp6_ia_clear_addresses(DHCP6IA *ia); +DHCP6IA *dhcp6_ia_free(DHCP6IA *ia); +DEFINE_TRIVIAL_CLEANUP_FUNC(DHCP6IA*, dhcp6_ia_free); + +bool dhcp6_option_can_request(uint16_t option); + +int dhcp6_option_append(uint8_t **buf, size_t *offset, uint16_t code, + size_t optlen, const void *optval); +int dhcp6_option_append_ia(uint8_t **buf, size_t *offset, const DHCP6IA *ia); +int dhcp6_option_append_fqdn(uint8_t **buf, size_t *offset, const char *fqdn); +int dhcp6_option_append_user_class(uint8_t **buf, size_t *offset, char * const *user_class); +int dhcp6_option_append_vendor_class(uint8_t **buf, size_t *offset, char * const *vendor_class); +int dhcp6_option_append_vendor_option(uint8_t **buf, size_t *offset, OrderedSet *vendor_options); + +int dhcp6_option_parse( + const uint8_t *buf, + size_t buflen, + size_t *offset, + uint16_t *ret_option_code, + size_t *ret_option_data_len, + const uint8_t **ret_option_data); +int dhcp6_option_parse_status(const uint8_t *data, size_t data_len, char **ret_status_message); +int dhcp6_option_parse_string(const uint8_t *data, size_t data_len, char **ret); +int dhcp6_option_parse_ia( + sd_dhcp6_client *client, + be32_t iaid, + uint16_t option_code, + size_t option_data_len, + const uint8_t *option_data, + DHCP6IA **ret); +int dhcp6_option_parse_addresses( + const uint8_t *optval, + size_t optlen, + struct in6_addr **addrs, + size_t *count); +int dhcp6_option_parse_domainname_list(const uint8_t *optval, size_t optlen, char ***ret); +int dhcp6_option_parse_domainname(const uint8_t *optval, size_t optlen, char **ret); diff --git a/src/libsystemd-network/dhcp6-protocol.c b/src/libsystemd-network/dhcp6-protocol.c new file mode 100644 index 0000000..be0f651 --- /dev/null +++ b/src/libsystemd-network/dhcp6-protocol.c @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "dhcp6-protocol.h" +#include "string-table.h" + +static const char * const dhcp6_state_table[_DHCP6_STATE_MAX] = { + [DHCP6_STATE_STOPPED] = "stopped", + [DHCP6_STATE_INFORMATION_REQUEST] = "information-request", + [DHCP6_STATE_SOLICITATION] = "solicitation", + [DHCP6_STATE_REQUEST] = "request", + [DHCP6_STATE_BOUND] = "bound", + [DHCP6_STATE_RENEW] = "renew", + [DHCP6_STATE_REBIND] = "rebind", + [DHCP6_STATE_STOPPING] = "stopping", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(dhcp6_state, DHCP6State); + +static const char * const dhcp6_message_type_table[_DHCP6_MESSAGE_TYPE_MAX] = { + [DHCP6_MESSAGE_SOLICIT] = "Solicit", + [DHCP6_MESSAGE_ADVERTISE] = "Advertise", + [DHCP6_MESSAGE_REQUEST] = "Request", + [DHCP6_MESSAGE_CONFIRM] = "Confirm", + [DHCP6_MESSAGE_RENEW] = "Renew", + [DHCP6_MESSAGE_REBIND] = "Rebind", + [DHCP6_MESSAGE_REPLY] = "Reply", + [DHCP6_MESSAGE_RELEASE] = "Release", + [DHCP6_MESSAGE_DECLINE] = "Decline", + [DHCP6_MESSAGE_RECONFIGURE] = "Reconfigure", + [DHCP6_MESSAGE_INFORMATION_REQUEST] = "Information Request", + [DHCP6_MESSAGE_RELAY_FORWARD] = "Relay Forward", + [DHCP6_MESSAGE_RELAY_REPLY] = "Relay Reply", + [DHCP6_MESSAGE_LEASE_QUERY] = "Lease Query", + [DHCP6_MESSAGE_LEASE_QUERY_REPLY] = "Lease Query Reply", + [DHCP6_MESSAGE_LEASE_QUERY_DONE] = "Lease Query Done", + [DHCP6_MESSAGE_LEASE_QUERY_DATA] = "Lease Query Data", + [DHCP6_MESSAGE_RECONFIGURE_REQUEST] = "Reconfigure Request", + [DHCP6_MESSAGE_RECONFIGURE_REPLY] = "Reconfigure Reply", + [DHCP6_MESSAGE_DHCPV4_QUERY] = "DHCPv4 Query", + [DHCP6_MESSAGE_DHCPV4_RESPONSE] = "DHCPv4 Response", + [DHCP6_MESSAGE_ACTIVE_LEASE_QUERY] = "Active Lease Query", + [DHCP6_MESSAGE_START_TLS] = "Start TLS", + [DHCP6_MESSAGE_BINDING_UPDATE] = "Binding Update", + [DHCP6_MESSAGE_BINDING_REPLY] = "Binding Reply", + [DHCP6_MESSAGE_POOL_REQUEST] = "Pool Request", + [DHCP6_MESSAGE_POOL_RESPONSE] = "Pool Response", + [DHCP6_MESSAGE_UPDATE_REQUEST] = "Update Request", + [DHCP6_MESSAGE_UPDATE_REQUEST_ALL] = "Update Request All", + [DHCP6_MESSAGE_UPDATE_DONE] = "Update Done", + [DHCP6_MESSAGE_CONNECT] = "Connect", + [DHCP6_MESSAGE_CONNECT_REPLY] = "Connect Reply", + [DHCP6_MESSAGE_DISCONNECT] = "Disconnect", + [DHCP6_MESSAGE_STATE] = "State", + [DHCP6_MESSAGE_CONTACT] = "Contact", +}; + +DEFINE_STRING_TABLE_LOOKUP(dhcp6_message_type, DHCP6MessageType); + +static const char * const dhcp6_message_status_table[_DHCP6_STATUS_MAX] = { + [DHCP6_STATUS_SUCCESS] = "Success", + [DHCP6_STATUS_UNSPEC_FAIL] = "Unspecified failure", + [DHCP6_STATUS_NO_ADDRS_AVAIL] = "No addresses available", + [DHCP6_STATUS_NO_BINDING] = "Binding unavailable", + [DHCP6_STATUS_NOT_ON_LINK] = "Not on link", + [DHCP6_STATUS_USE_MULTICAST] = "Use multicast", + [DHCP6_STATUS_NO_PREFIX_AVAIL] = "No prefix available", + [DHCP6_STATUS_UNKNOWN_QUERY_TYPE] = "Unknown query type", + [DHCP6_STATUS_MALFORMED_QUERY] = "Malformed query", + [DHCP6_STATUS_NOT_CONFIGURED] = "Not configured", + [DHCP6_STATUS_NOT_ALLOWED] = "Not allowed", + [DHCP6_STATUS_QUERY_TERMINATED] = "Query terminated", + [DHCP6_STATUS_DATA_MISSING] = "Data missing", + [DHCP6_STATUS_CATCHUP_COMPLETE] = "Catch up complete", + [DHCP6_STATUS_NOT_SUPPORTED] = "Not supported", + [DHCP6_STATUS_TLS_CONNECTION_REFUSED] = "TLS connection refused", + [DHCP6_STATUS_ADDRESS_IN_USE] = "Address in use", + [DHCP6_STATUS_CONFIGURATION_CONFLICT] = "Configuration conflict", + [DHCP6_STATUS_MISSING_BINDING_INFORMATION] = "Missing binding information", + [DHCP6_STATUS_OUTDATED_BINDING_INFORMATION] = "Outdated binding information", + [DHCP6_STATUS_SERVER_SHUTTING_DOWN] = "Server shutting down", + [DHCP6_STATUS_DNS_UPDATE_NOT_SUPPORTED] = "DNS update not supported", + [DHCP6_STATUS_EXCESSIVE_TIME_SKEW] = "Excessive time skew", +}; + +DEFINE_STRING_TABLE_LOOKUP(dhcp6_message_status, DHCP6Status); + +int dhcp6_message_status_to_errno(DHCP6Status s) { + switch (s) { + case DHCP6_STATUS_SUCCESS: + return 0; + case DHCP6_STATUS_NO_BINDING: + return -EADDRNOTAVAIL; + default: + return -EINVAL; + } +} diff --git a/src/libsystemd-network/dhcp6-protocol.h b/src/libsystemd-network/dhcp6-protocol.h new file mode 100644 index 0000000..c70f932 --- /dev/null +++ b/src/libsystemd-network/dhcp6-protocol.h @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <netinet/ip6.h> +#include <netinet/udp.h> + +#include "macro.h" +#include "sparse-endian.h" + +struct DHCP6Message { + union { + struct { + uint8_t type; + uint8_t _pad[3]; + } _packed_; + be32_t transaction_id; + }; + uint8_t options[]; +} _packed_; + +typedef struct DHCP6Message DHCP6Message; + +#define DHCP6_MIN_OPTIONS_SIZE \ + 1280 - sizeof(struct ip6_hdr) - sizeof(struct udphdr) + +#define IN6ADDR_ALL_DHCP6_RELAY_AGENTS_AND_SERVERS_INIT \ + { { { 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02 } } } + +enum { + DHCP6_PORT_SERVER = 547, + DHCP6_PORT_CLIENT = 546, +}; + +#define DHCP6_INF_TIMEOUT (1 * USEC_PER_SEC) +#define DHCP6_INF_MAX_RT (120 * USEC_PER_SEC) +#define DHCP6_SOL_MAX_DELAY (1 * USEC_PER_SEC) +#define DHCP6_SOL_TIMEOUT (1 * USEC_PER_SEC) +#define DHCP6_SOL_MAX_RT (120 * USEC_PER_SEC) +#define DHCP6_REQ_TIMEOUT (1 * USEC_PER_SEC) +#define DHCP6_REQ_MAX_RT (120 * USEC_PER_SEC) +#define DHCP6_REQ_MAX_RC 10 +#define DHCP6_REN_TIMEOUT (10 * USEC_PER_SEC) +#define DHCP6_REN_MAX_RT (600 * USEC_PER_SEC) +#define DHCP6_REB_TIMEOUT (10 * USEC_PER_SEC) +#define DHCP6_REB_MAX_RT (600 * USEC_PER_SEC) + +typedef enum DHCP6State { + DHCP6_STATE_STOPPED, + DHCP6_STATE_INFORMATION_REQUEST, + DHCP6_STATE_SOLICITATION, + DHCP6_STATE_REQUEST, + DHCP6_STATE_BOUND, + DHCP6_STATE_RENEW, + DHCP6_STATE_REBIND, + DHCP6_STATE_STOPPING, + _DHCP6_STATE_MAX, + _DHCP6_STATE_INVALID = -EINVAL, +} DHCP6State; + +/* https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#dhcpv6-parameters-1 */ +typedef enum DHCP6MessageType { + DHCP6_MESSAGE_SOLICIT = 1, /* RFC 8415 */ + DHCP6_MESSAGE_ADVERTISE = 2, /* RFC 8415 */ + DHCP6_MESSAGE_REQUEST = 3, /* RFC 8415 */ + DHCP6_MESSAGE_CONFIRM = 4, /* RFC 8415 */ + DHCP6_MESSAGE_RENEW = 5, /* RFC 8415 */ + DHCP6_MESSAGE_REBIND = 6, /* RFC 8415 */ + DHCP6_MESSAGE_REPLY = 7, /* RFC 8415 */ + DHCP6_MESSAGE_RELEASE = 8, /* RFC 8415 */ + DHCP6_MESSAGE_DECLINE = 9, /* RFC 8415 */ + DHCP6_MESSAGE_RECONFIGURE = 10, /* RFC 8415 */ + DHCP6_MESSAGE_INFORMATION_REQUEST = 11, /* RFC 8415 */ + DHCP6_MESSAGE_RELAY_FORWARD = 12, /* RFC 8415 */ + DHCP6_MESSAGE_RELAY_REPLY = 13, /* RFC 8415 */ + DHCP6_MESSAGE_LEASE_QUERY = 14, /* RFC 5007 */ + DHCP6_MESSAGE_LEASE_QUERY_REPLY = 15, /* RFC 5007 */ + DHCP6_MESSAGE_LEASE_QUERY_DONE = 16, /* RFC 5460 */ + DHCP6_MESSAGE_LEASE_QUERY_DATA = 17, /* RFC 5460 */ + DHCP6_MESSAGE_RECONFIGURE_REQUEST = 18, /* RFC 6977 */ + DHCP6_MESSAGE_RECONFIGURE_REPLY = 19, /* RFC 6977 */ + DHCP6_MESSAGE_DHCPV4_QUERY = 20, /* RFC 7341 */ + DHCP6_MESSAGE_DHCPV4_RESPONSE = 21, /* RFC 7341 */ + DHCP6_MESSAGE_ACTIVE_LEASE_QUERY = 22, /* RFC 7653 */ + DHCP6_MESSAGE_START_TLS = 23, /* RFC 7653 */ + DHCP6_MESSAGE_BINDING_UPDATE = 24, /* RFC 8156 */ + DHCP6_MESSAGE_BINDING_REPLY = 25, /* RFC 8156 */ + DHCP6_MESSAGE_POOL_REQUEST = 26, /* RFC 8156 */ + DHCP6_MESSAGE_POOL_RESPONSE = 27, /* RFC 8156 */ + DHCP6_MESSAGE_UPDATE_REQUEST = 28, /* RFC 8156 */ + DHCP6_MESSAGE_UPDATE_REQUEST_ALL = 29, /* RFC 8156 */ + DHCP6_MESSAGE_UPDATE_DONE = 30, /* RFC 8156 */ + DHCP6_MESSAGE_CONNECT = 31, /* RFC 8156 */ + DHCP6_MESSAGE_CONNECT_REPLY = 32, /* RFC 8156 */ + DHCP6_MESSAGE_DISCONNECT = 33, /* RFC 8156 */ + DHCP6_MESSAGE_STATE = 34, /* RFC 8156 */ + DHCP6_MESSAGE_CONTACT = 35, /* RFC 8156 */ + _DHCP6_MESSAGE_TYPE_MAX, + _DHCP6_MESSAGE_TYPE_INVALID = -EINVAL, +} DHCP6MessageType; + +typedef enum DHCP6NTPSubOption { + DHCP6_NTP_SUBOPTION_SRV_ADDR = 1, + DHCP6_NTP_SUBOPTION_MC_ADDR = 2, + DHCP6_NTP_SUBOPTION_SRV_FQDN = 3, + _DHCP6_NTP_SUBOPTION_MAX, + _DHCP6_NTP_SUBOPTION_INVALID = -EINVAL, +} DHCP6NTPSubOption; + +/* + * RFC 8415, RFC 5007 and RFC 7653 status codes: + * https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#dhcpv6-parameters-5 + */ +typedef enum DHCP6Status { + DHCP6_STATUS_SUCCESS = 0, + DHCP6_STATUS_UNSPEC_FAIL = 1, + DHCP6_STATUS_NO_ADDRS_AVAIL = 2, + DHCP6_STATUS_NO_BINDING = 3, + DHCP6_STATUS_NOT_ON_LINK = 4, + DHCP6_STATUS_USE_MULTICAST = 5, + DHCP6_STATUS_NO_PREFIX_AVAIL = 6, + DHCP6_STATUS_UNKNOWN_QUERY_TYPE = 7, + DHCP6_STATUS_MALFORMED_QUERY = 8, + DHCP6_STATUS_NOT_CONFIGURED = 9, + DHCP6_STATUS_NOT_ALLOWED = 10, + DHCP6_STATUS_QUERY_TERMINATED = 11, + DHCP6_STATUS_DATA_MISSING = 12, + DHCP6_STATUS_CATCHUP_COMPLETE = 13, + DHCP6_STATUS_NOT_SUPPORTED = 14, + DHCP6_STATUS_TLS_CONNECTION_REFUSED = 15, + DHCP6_STATUS_ADDRESS_IN_USE = 16, + DHCP6_STATUS_CONFIGURATION_CONFLICT = 17, + DHCP6_STATUS_MISSING_BINDING_INFORMATION = 18, + DHCP6_STATUS_OUTDATED_BINDING_INFORMATION = 19, + DHCP6_STATUS_SERVER_SHUTTING_DOWN = 20, + DHCP6_STATUS_DNS_UPDATE_NOT_SUPPORTED = 21, + DHCP6_STATUS_EXCESSIVE_TIME_SKEW = 22, + _DHCP6_STATUS_MAX, + _DHCP6_STATUS_INVALID = -EINVAL, +} DHCP6Status; + +typedef enum DHCP6FQDNFlag { + DHCP6_FQDN_FLAG_S = 1 << 0, + DHCP6_FQDN_FLAG_O = 1 << 1, + DHCP6_FQDN_FLAG_N = 1 << 2, +} DHCP6FQDNFlag; + +const char *dhcp6_state_to_string(DHCP6State s) _const_; +const char *dhcp6_message_type_to_string(DHCP6MessageType s) _const_; +DHCP6MessageType dhcp6_message_type_from_string(const char *s) _pure_; +const char *dhcp6_message_status_to_string(DHCP6Status s) _const_; +DHCP6Status dhcp6_message_status_from_string(const char *s) _pure_; +int dhcp6_message_status_to_errno(DHCP6Status s); diff --git a/src/libsystemd-network/fuzz-dhcp-client.c b/src/libsystemd-network/fuzz-dhcp-client.c new file mode 100644 index 0000000..384972f --- /dev/null +++ b/src/libsystemd-network/fuzz-dhcp-client.c @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "sd-dhcp-client.c" + +#include "alloc-util.h" +#include "dhcp-network.h" +#include "fuzz.h" + +int dhcp_network_bind_raw_socket( + int ifindex, + union sockaddr_union *link, + uint32_t id, + const struct hw_addr_data *hw_addr, + const struct hw_addr_data *bcast_addr, + uint16_t arp_type, + uint16_t port, + bool so_priority_set, + int so_priority) { + + int fd; + fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + return fd; +} + +int dhcp_network_send_raw_socket(int s, const union sockaddr_union *link, const void *packet, size_t len) { + return len; +} + +int dhcp_network_bind_udp_socket(int ifindex, be32_t address, uint16_t port, int ip_service_type) { + int fd; + + fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + return fd; +} + +int dhcp_network_send_udp_socket(int s, be32_t address, uint16_t port, const void *packet, size_t len) { + return len; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + uint8_t mac_addr[] = {'A', 'B', 'C', '1', '2', '3'}; + uint8_t bcast_addr[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + _cleanup_(sd_dhcp_client_unrefp) sd_dhcp_client *client = NULL; + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + int res, r; + + assert_se(setenv("SYSTEMD_NETWORK_TEST_MODE", "1", 1) >= 0); + + fuzz_setup_logging(); + + r = sd_dhcp_client_new(&client, false); + assert_se(r >= 0); + assert_se(client); + + assert_se(sd_event_new(&e) >= 0); + + r = sd_dhcp_client_attach_event(client, e, 0); + assert_se(r >= 0); + + assert_se(sd_dhcp_client_set_ifindex(client, 42) >= 0); + assert_se(sd_dhcp_client_set_mac(client, mac_addr, bcast_addr, ETH_ALEN, ARPHRD_ETHER) >= 0); + + res = sd_dhcp_client_start(client); + assert_se(IN_SET(res, 0, -EINPROGRESS)); + client->xid = 2; + client->state = DHCP_STATE_SELECTING; + + (void) client_handle_offer_or_rapid_ack(client, (DHCPMessage*) data, size, NULL); + + assert_se(sd_dhcp_client_stop(client) >= 0); + + return 0; +} diff --git a/src/libsystemd-network/fuzz-dhcp-server-relay.c b/src/libsystemd-network/fuzz-dhcp-server-relay.c new file mode 100644 index 0000000..5520003 --- /dev/null +++ b/src/libsystemd-network/fuzz-dhcp-server-relay.c @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include "sd-dhcp-server.c" + +#include "fuzz.h" + +ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) { + return len; +} + +ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags) { + return 0; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; + struct in_addr address = {.s_addr = htobe32(UINT32_C(10) << 24 | UINT32_C(1))}; + union in_addr_union relay_address; + _cleanup_free_ uint8_t *message = NULL; + + if (size < sizeof(DHCPMessage)) + return 0; + + fuzz_setup_logging(); + + assert_se(sd_dhcp_server_new(&server, 1) >= 0); + assert_se(sd_dhcp_server_attach_event(server, NULL, 0) >= 0); + assert_se(sd_dhcp_server_configure_pool(server, &address, 24, 0, 0) >= 0); + assert_se(in_addr_from_string(AF_INET, "192.168.5.1", &relay_address) >= 0); + assert_se(sd_dhcp_server_set_relay_target(server, &relay_address.in) >= 0); + assert_se(sd_dhcp_server_set_bind_to_interface(server, false) >= 0); + assert_se(sd_dhcp_server_set_relay_agent_information(server, "string:sample_circuit_id", "string:sample_remote_id") >= 0); + + size_t buflen = size; + buflen += relay_agent_information_length(server->agent_circuit_id, server->agent_remote_id) + 2; + assert_se(message = malloc(buflen)); + memcpy(message, data, size); + + server->fd = open("/dev/null", O_RDWR|O_CLOEXEC|O_NOCTTY); + assert_se(server->fd >= 0); + + (void) dhcp_server_relay_message(server, (DHCPMessage *) message, size - sizeof(DHCPMessage), buflen); + return 0; +} diff --git a/src/libsystemd-network/fuzz-dhcp-server.c b/src/libsystemd-network/fuzz-dhcp-server.c new file mode 100644 index 0000000..fddb3a5 --- /dev/null +++ b/src/libsystemd-network/fuzz-dhcp-server.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include "sd-dhcp-server.c" + +#include "fuzz.h" + +/* stub out network so that the server doesn't send */ +ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) { + return len; +} + +ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags) { + return 0; +} + +static int add_lease(sd_dhcp_server *server, const struct in_addr *server_address, uint8_t i) { + _cleanup_(dhcp_lease_freep) DHCPLease *lease = NULL; + int r; + + assert(server); + + lease = new(DHCPLease, 1); + if (!lease) + return -ENOMEM; + + *lease = (DHCPLease) { + .address = htobe32(UINT32_C(10) << 24 | i), + .chaddr = { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 }, + .expiration = UINT64_MAX, + .gateway = server_address->s_addr, + .hlen = ETH_ALEN, + .htype = ARPHRD_ETHER, + + .client_id.length = 2, + }; + + lease->client_id.data = new(uint8_t, lease->client_id.length); + if (!lease->client_id.data) + return -ENOMEM; + + lease->client_id.data[0] = 2; + lease->client_id.data[1] = i; + + lease->server = server; /* This must be set just before hashmap_put(). */ + + r = hashmap_ensure_put(&server->bound_leases_by_client_id, &dhcp_lease_hash_ops, &lease->client_id, lease); + if (r < 0) + return r; + + r = hashmap_ensure_put(&server->bound_leases_by_address, NULL, UINT32_TO_PTR(lease->address), lease); + if (r < 0) + return r; + + TAKE_PTR(lease); + return 0; +} + +static int add_static_lease(sd_dhcp_server *server, uint8_t i) { + uint8_t id[2] = { 2, i }; + + assert(server); + + return sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = htobe32(UINT32_C(10) << 24 | i)}, + id, ELEMENTSOF(id)); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; + struct in_addr address = { .s_addr = htobe32(UINT32_C(10) << 24 | UINT32_C(1))}; + _cleanup_free_ uint8_t *duped = NULL; + + if (size < sizeof(DHCPMessage)) + return 0; + + fuzz_setup_logging(); + + assert_se(duped = memdup(data, size)); + + assert_se(sd_dhcp_server_new(&server, 1) >= 0); + assert_se(sd_dhcp_server_attach_event(server, NULL, 0) >= 0); + server->fd = open("/dev/null", O_RDWR|O_CLOEXEC|O_NOCTTY); + assert_se(server->fd >= 0); + assert_se(sd_dhcp_server_configure_pool(server, &address, 24, 0, 0) >= 0); + + /* add leases to the pool to expose additional code paths */ + assert_se(add_lease(server, &address, 2) >= 0); + assert_se(add_lease(server, &address, 3) >= 0); + + /* add static leases */ + assert_se(add_static_lease(server, 3) >= 0); + assert_se(add_static_lease(server, 4) >= 0); + + (void) dhcp_server_handle_message(server, (DHCPMessage*) duped, size, NULL); + + return 0; +} diff --git a/src/libsystemd-network/fuzz-dhcp6-client.c b/src/libsystemd-network/fuzz-dhcp6-client.c new file mode 100644 index 0000000..2d42844 --- /dev/null +++ b/src/libsystemd-network/fuzz-dhcp6-client.c @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <unistd.h> + +#include "sd-dhcp6-client.h" +#include "sd-event.h" + +#include "dhcp6-internal.h" +#include "event-util.h" +#include "fd-util.h" +#include "fuzz.h" + +static int test_dhcp_fd[2] = EBADF_PAIR; + +int dhcp6_network_send_udp_socket(int s, struct in6_addr *server_address, const void *packet, size_t len) { + return len; +} + +int dhcp6_network_bind_udp_socket(int index, struct in6_addr *local_address) { + assert_se(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_dhcp_fd) >= 0); + return TAKE_FD(test_dhcp_fd[0]); +} + +static void fuzz_client(sd_dhcp6_client *client, const uint8_t *data, size_t size, DHCP6State state) { + assert_se(sd_dhcp6_client_set_information_request(client, state == DHCP6_STATE_INFORMATION_REQUEST) >= 0); + assert_se(sd_dhcp6_client_start(client) >= 0); + + client->state = state; + + if (size >= sizeof(DHCP6Message)) + assert_se(dhcp6_client_set_transaction_id(client, ((const DHCP6Message *) data)->transaction_id) == 0); + + /* These states does not require lease to send message. */ + if (IN_SET(client->state, DHCP6_STATE_INFORMATION_REQUEST, DHCP6_STATE_SOLICITATION)) + assert_se(dhcp6_client_send_message(client) >= 0); + + assert_se(write(test_dhcp_fd[1], data, size) == (ssize_t) size); + + assert_se(sd_event_run(sd_dhcp6_client_get_event(client), UINT64_MAX) > 0); + + /* Check the state transition. */ + if (client->state != state) + switch (state) { + case DHCP6_STATE_INFORMATION_REQUEST: + assert_se(client->state == DHCP6_STATE_STOPPED); + break; + case DHCP6_STATE_SOLICITATION: + assert_se(IN_SET(client->state, DHCP6_STATE_REQUEST, DHCP6_STATE_BOUND)); + break; + case DHCP6_STATE_REQUEST: + assert_se(IN_SET(client->state, DHCP6_STATE_BOUND, DHCP6_STATE_SOLICITATION)); + break; + default: + assert_not_reached(); + } + + /* Send message if the client has a lease. */ + if (state != DHCP6_STATE_INFORMATION_REQUEST && sd_dhcp6_client_get_lease(client, NULL) >= 0) { + client->state = DHCP6_STATE_REQUEST; + dhcp6_client_send_message(client); + } + + assert_se(sd_dhcp6_client_stop(client) >= 0); + + test_dhcp_fd[1] = safe_close(test_dhcp_fd[1]); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL; + _cleanup_(sd_dhcp6_option_unrefp) sd_dhcp6_option *v1 = NULL, *v2 = NULL; + struct in6_addr address = { { { 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 } } }; + struct in6_addr hint = { { { 0x3f, 0xfe, 0x05, 0x01, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } }; + static const char *v1_data = "hogehoge", *v2_data = "foobar"; + + assert_se(setenv("SYSTEMD_NETWORK_TEST_MODE", "1", 1) >= 0); + + fuzz_setup_logging(); + + if (outside_size_range(size, 0, 65536)) + return 0; + + assert_se(sd_event_new(&e) >= 0); + assert_se(sd_dhcp6_client_new(&client) >= 0); + assert_se(sd_dhcp6_client_attach_event(client, e, 0) >= 0); + assert_se(sd_dhcp6_client_set_ifindex(client, 42) >= 0); + assert_se(sd_dhcp6_client_set_local_address(client, &address) >= 0); + + /* Used when sending message. */ + assert_se(sd_dhcp6_client_set_fqdn(client, "example.com") == 1); + assert_se(sd_dhcp6_client_set_request_mud_url(client, "https://www.example.com/mudfile.json") >= 0); + assert_se(sd_dhcp6_client_set_request_user_class(client, STRV_MAKE("u1", "u2", "u3")) >= 0); + assert_se(sd_dhcp6_client_set_request_vendor_class(client, STRV_MAKE("v1", "v2", "v3")) >= 0); + assert_se(sd_dhcp6_client_set_prefix_delegation_hint(client, 48, &hint) >= 0); + assert_se(sd_dhcp6_option_new(123, v1_data, strlen(v1_data), 12345, &v1) >= 0); + assert_se(sd_dhcp6_option_new(456, v2_data, strlen(v2_data), 45678, &v2) >= 0); + assert_se(sd_dhcp6_client_add_vendor_option(client, v1) >= 0); + assert_se(sd_dhcp6_client_add_vendor_option(client, v2) >= 0); + + fuzz_client(client, data, size, DHCP6_STATE_INFORMATION_REQUEST); + fuzz_client(client, data, size, DHCP6_STATE_SOLICITATION); + + /* If size is zero, then the resend timer will be triggered at first, + * but in the REQUEST state the client must have a lease. */ + if (size == 0) + return 0; + + fuzz_client(client, data, size, DHCP6_STATE_REQUEST); + + return 0; +} diff --git a/src/libsystemd-network/fuzz-dhcp6-client.options b/src/libsystemd-network/fuzz-dhcp6-client.options new file mode 100644 index 0000000..678d526 --- /dev/null +++ b/src/libsystemd-network/fuzz-dhcp6-client.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 65536 diff --git a/src/libsystemd-network/fuzz-lldp-rx.c b/src/libsystemd-network/fuzz-lldp-rx.c new file mode 100644 index 0000000..844957c --- /dev/null +++ b/src/libsystemd-network/fuzz-lldp-rx.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <unistd.h> + +#include "sd-event.h" +#include "sd-lldp-rx.h" + +#include "fd-util.h" +#include "fuzz.h" +#include "lldp-network.h" + +static int test_fd[2] = EBADF_PAIR; + +int lldp_network_bind_raw_socket(int ifindex) { + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_fd) < 0) + return -errno; + + return test_fd[0]; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_lldp_rx_unrefp) sd_lldp_rx *lldp_rx = NULL; + + if (outside_size_range(size, 0, 2048)) + return 0; + + fuzz_setup_logging(); + + assert_se(sd_event_new(&e) == 0); + assert_se(sd_lldp_rx_new(&lldp_rx) >= 0); + assert_se(sd_lldp_rx_set_ifindex(lldp_rx, 42) >= 0); + assert_se(sd_lldp_rx_attach_event(lldp_rx, e, 0) >= 0); + assert_se(sd_lldp_rx_start(lldp_rx) >= 0); + + assert_se(write(test_fd[1], data, size) == (ssize_t) size); + assert_se(sd_event_run(e, 0) >= 0); + + assert_se(sd_lldp_rx_stop(lldp_rx) >= 0); + assert_se(sd_lldp_rx_detach_event(lldp_rx) >= 0); + test_fd[1] = safe_close(test_fd[1]); + + return 0; +} diff --git a/src/libsystemd-network/fuzz-lldp-rx.options b/src/libsystemd-network/fuzz-lldp-rx.options new file mode 100644 index 0000000..60bd9b0 --- /dev/null +++ b/src/libsystemd-network/fuzz-lldp-rx.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 2048 diff --git a/src/libsystemd-network/fuzz-ndisc-rs.c b/src/libsystemd-network/fuzz-ndisc-rs.c new file mode 100644 index 0000000..a89e2b0 --- /dev/null +++ b/src/libsystemd-network/fuzz-ndisc-rs.c @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <netinet/icmp6.h> +#include <unistd.h> + +#include "sd-ndisc.h" + +#include "alloc-util.h" +#include "fd-util.h" +#include "fuzz.h" +#include "icmp6-util-unix.h" +#include "ndisc-internal.h" +#include "socket-util.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + struct ether_addr mac_addr = { + .ether_addr_octet = {'A', 'B', 'C', '1', '2', '3'} + }; + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_ndisc_unrefp) sd_ndisc *nd = NULL; + + if (outside_size_range(size, 0, 2048)) + return 0; + + fuzz_setup_logging(); + + assert_se(sd_event_new(&e) >= 0); + assert_se(sd_ndisc_new(&nd) >= 0); + assert_se(sd_ndisc_attach_event(nd, e, 0) >= 0); + assert_se(sd_ndisc_set_ifindex(nd, 42) >= 0); + assert_se(sd_ndisc_set_mac(nd, &mac_addr) >= 0); + assert_se(sd_ndisc_start(nd) >= 0); + assert_se(write(test_fd[1], data, size) == (ssize_t) size); + (void) sd_event_run(e, UINT64_MAX); + assert_se(sd_ndisc_stop(nd) >= 0); + close(test_fd[1]); + + return 0; +} diff --git a/src/libsystemd-network/fuzz-ndisc-rs.options b/src/libsystemd-network/fuzz-ndisc-rs.options new file mode 100644 index 0000000..60bd9b0 --- /dev/null +++ b/src/libsystemd-network/fuzz-ndisc-rs.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 2048 diff --git a/src/libsystemd-network/icmp6-util-unix.c b/src/libsystemd-network/icmp6-util-unix.c new file mode 100644 index 0000000..01edb85 --- /dev/null +++ b/src/libsystemd-network/icmp6-util-unix.c @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/ip6.h> +#include <unistd.h> + +#include "fd-util.h" +#include "icmp6-util-unix.h" + +send_ra_t send_ra_function = NULL; +int test_fd[2] = EBADF_PAIR; + +static struct in6_addr dummy_link_local = { + .s6_addr = { + 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x12, 0x34, 0x56, 0xff, 0xfe, 0x78, 0x9a, 0xbc, + }, +}; + +int icmp6_bind_router_solicitation(int ifindex) { + if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_fd) < 0) + return -errno; + + return test_fd[0]; +} + +int icmp6_bind_router_advertisement(int ifindex) { + return test_fd[1]; +} + +int icmp6_send_router_solicitation(int s, const struct ether_addr *ether_addr) { + if (!send_ra_function) + return 0; + + return send_ra_function(0); +} + +int icmp6_receive( + int fd, + void *iov_base, + size_t iov_len, + struct in6_addr *ret_sender, + triple_timestamp *ret_timestamp) { + + assert_se(read (fd, iov_base, iov_len) == (ssize_t) iov_len); + + if (ret_timestamp) + triple_timestamp_now(ret_timestamp); + + if (ret_sender) + *ret_sender = dummy_link_local; + + return 0; +} diff --git a/src/libsystemd-network/icmp6-util-unix.h b/src/libsystemd-network/icmp6-util-unix.h new file mode 100644 index 0000000..a9cb05a --- /dev/null +++ b/src/libsystemd-network/icmp6-util-unix.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "icmp6-util.h" + +typedef int (*send_ra_t)(uint8_t flags); + +extern send_ra_t send_ra_function; +extern int test_fd[2]; diff --git a/src/libsystemd-network/icmp6-util.c b/src/libsystemd-network/icmp6-util.c new file mode 100644 index 0000000..72c20ba --- /dev/null +++ b/src/libsystemd-network/icmp6-util.c @@ -0,0 +1,203 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <netinet/icmp6.h> +#include <netinet/in.h> +#include <netinet/ip6.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> +#include <net/if.h> +#include <linux/if_packet.h> + +#include "fd-util.h" +#include "icmp6-util.h" +#include "in-addr-util.h" +#include "iovec-util.h" +#include "network-common.h" +#include "socket-util.h" + +#define IN6ADDR_ALL_ROUTERS_MULTICAST_INIT \ + { { { 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 } } } + +#define IN6ADDR_ALL_NODES_MULTICAST_INIT \ + { { { 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 } } } + +static int icmp6_bind_router_message(const struct icmp6_filter *filter, + const struct ipv6_mreq *mreq) { + int ifindex = mreq->ipv6mr_interface; + _cleanup_close_ int s = -EBADF; + int r; + + assert(filter); + assert(mreq); + + s = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_ICMPV6); + if (s < 0) + return -errno; + + if (setsockopt(s, IPPROTO_ICMPV6, ICMP6_FILTER, filter, sizeof(*filter)) < 0) + return -errno; + + if (setsockopt(s, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, mreq, sizeof(*mreq)) < 0) + return -errno; + + /* RFC 3315, section 6.7, bullet point 2 may indicate that an + IPV6_PKTINFO socket option also applies for ICMPv6 multicast. + Empirical experiments indicates otherwise and therefore an + IPV6_MULTICAST_IF socket option is used here instead */ + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_IF, ifindex); + if (r < 0) + return r; + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, false); + if (r < 0) + return r; + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, 255); + if (r < 0) + return r; + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, 255); + if (r < 0) + return r; + + r = setsockopt_int(s, SOL_IPV6, IPV6_RECVHOPLIMIT, true); + if (r < 0) + return r; + + r = setsockopt_int(s, SOL_SOCKET, SO_TIMESTAMP, true); + if (r < 0) + return r; + + r = socket_bind_to_ifindex(s, ifindex); + if (r < 0) + return r; + + return TAKE_FD(s); +} + +int icmp6_bind_router_solicitation(int ifindex) { + struct icmp6_filter filter = {}; + struct ipv6_mreq mreq = { + .ipv6mr_multiaddr = IN6ADDR_ALL_NODES_MULTICAST_INIT, + .ipv6mr_interface = ifindex, + }; + + ICMP6_FILTER_SETBLOCKALL(&filter); + ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter); + + return icmp6_bind_router_message(&filter, &mreq); +} + +int icmp6_bind_router_advertisement(int ifindex) { + struct icmp6_filter filter = {}; + struct ipv6_mreq mreq = { + .ipv6mr_multiaddr = IN6ADDR_ALL_ROUTERS_MULTICAST_INIT, + .ipv6mr_interface = ifindex, + }; + + ICMP6_FILTER_SETBLOCKALL(&filter); + ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter); + + return icmp6_bind_router_message(&filter, &mreq); +} + +int icmp6_send_router_solicitation(int s, const struct ether_addr *ether_addr) { + struct sockaddr_in6 dst = { + .sin6_family = AF_INET6, + .sin6_addr = IN6ADDR_ALL_ROUTERS_MULTICAST_INIT, + }; + struct { + struct nd_router_solicit rs; + struct nd_opt_hdr rs_opt; + struct ether_addr rs_opt_mac; + } _packed_ rs = { + .rs.nd_rs_type = ND_ROUTER_SOLICIT, + .rs_opt.nd_opt_type = ND_OPT_SOURCE_LINKADDR, + .rs_opt.nd_opt_len = 1, + }; + struct iovec iov = { + .iov_base = &rs, + .iov_len = sizeof(rs), + }; + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + + assert(s >= 0); + assert(ether_addr); + + rs.rs_opt_mac = *ether_addr; + + if (sendmsg(s, &msg, 0) < 0) + return -errno; + + return 0; +} + +int icmp6_receive( + int fd, + void *buffer, + size_t size, + struct in6_addr *ret_sender, + triple_timestamp *ret_timestamp) { + + /* This needs to be initialized with zero. See #20741. */ + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(int)) + /* ttl */ + CMSG_SPACE_TIMEVAL) control = {}; + struct iovec iov = {}; + union sockaddr_union sa = {}; + struct msghdr msg = { + .msg_name = &sa.sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct in6_addr addr = {}; + ssize_t len; + + iov = IOVEC_MAKE(buffer, size); + + len = recvmsg_safe(fd, &msg, MSG_DONTWAIT); + if (len < 0) + return (int) len; + + if ((size_t) len != size) + return -EINVAL; + + if (msg.msg_namelen == sizeof(struct sockaddr_in6) && + sa.in6.sin6_family == AF_INET6) { + + addr = sa.in6.sin6_addr; + if (!in6_addr_is_link_local(&addr) && !in6_addr_is_null(&addr)) + return -EADDRNOTAVAIL; + + } else if (msg.msg_namelen > 0) + return -EPFNOSUPPORT; + + /* namelen == 0 only happens when running the test-suite over a socketpair */ + + assert(!(msg.msg_flags & MSG_TRUNC)); + + int *hops = CMSG_FIND_DATA(&msg, SOL_IPV6, IPV6_HOPLIMIT, int); + if (hops && *hops != 255) + return -EMULTIHOP; + + if (ret_timestamp) + triple_timestamp_from_cmsg(ret_timestamp, &msg); + if (ret_sender) + *ret_sender = addr; + return 0; +} diff --git a/src/libsystemd-network/icmp6-util.h b/src/libsystemd-network/icmp6-util.h new file mode 100644 index 0000000..0a9ecb4 --- /dev/null +++ b/src/libsystemd-network/icmp6-util.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014-2015 Intel Corporation. All rights reserved. +***/ + +#include <net/ethernet.h> + +#include "time-util.h" + +#define IN6ADDR_ALL_ROUTERS_MULTICAST_INIT \ + { { { 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 } } } + +#define IN6ADDR_ALL_NODES_MULTICAST_INIT \ + { { { 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 } } } + +int icmp6_bind_router_solicitation(int ifindex); +int icmp6_bind_router_advertisement(int ifindex); +int icmp6_send_router_solicitation(int s, const struct ether_addr *ether_addr); +int icmp6_receive( + int fd, + void *buffer, + size_t size, + struct in6_addr *ret_sender, + triple_timestamp *ret_timestamp); diff --git a/src/libsystemd-network/lldp-neighbor.c b/src/libsystemd-network/lldp-neighbor.c new file mode 100644 index 0000000..af61c9b --- /dev/null +++ b/src/libsystemd-network/lldp-neighbor.c @@ -0,0 +1,795 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "escape.h" +#include "ether-addr-util.h" +#include "hexdecoct.h" +#include "in-addr-util.h" +#include "lldp-neighbor.h" +#include "memory-util.h" +#include "missing_network.h" +#include "unaligned.h" + +static void lldp_neighbor_id_hash_func(const LLDPNeighborID *id, struct siphash *state) { + assert(id); + assert(state); + + siphash24_compress(id->chassis_id, id->chassis_id_size, state); + siphash24_compress(&id->chassis_id_size, sizeof(id->chassis_id_size), state); + siphash24_compress(id->port_id, id->port_id_size, state); + siphash24_compress(&id->port_id_size, sizeof(id->port_id_size), state); +} + +int lldp_neighbor_id_compare_func(const LLDPNeighborID *x, const LLDPNeighborID *y) { + assert(x); + assert(y); + + return memcmp_nn(x->chassis_id, x->chassis_id_size, y->chassis_id, y->chassis_id_size) + ?: memcmp_nn(x->port_id, x->port_id_size, y->port_id, y->port_id_size); +} + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + lldp_neighbor_hash_ops, + LLDPNeighborID, + lldp_neighbor_id_hash_func, + lldp_neighbor_id_compare_func, + sd_lldp_neighbor, + lldp_neighbor_unlink); + +int lldp_neighbor_prioq_compare_func(const void *a, const void *b) { + const sd_lldp_neighbor *x = a, *y = b; + + assert(x); + assert(y); + + return CMP(x->until, y->until); +} + +sd_lldp_neighbor *sd_lldp_neighbor_ref(sd_lldp_neighbor *n) { + if (!n) + return NULL; + + assert(n->n_ref > 0 || n->lldp_rx); + n->n_ref++; + + return n; +} + +static sd_lldp_neighbor *lldp_neighbor_free(sd_lldp_neighbor *n) { + if (!n) + return NULL; + + free(n->id.port_id); + free(n->id.chassis_id); + free(n->port_description); + free(n->system_name); + free(n->system_description); + free(n->mud_url); + free(n->chassis_id_as_string); + free(n->port_id_as_string); + return mfree(n); +} + +sd_lldp_neighbor *sd_lldp_neighbor_unref(sd_lldp_neighbor *n) { + + /* Drops one reference from the neighbor. Note that the object is not freed unless it is already unlinked from + * the sd_lldp object. */ + + if (!n) + return NULL; + + assert(n->n_ref > 0); + n->n_ref--; + + if (n->n_ref <= 0 && !n->lldp_rx) + lldp_neighbor_free(n); + + return NULL; +} + +sd_lldp_neighbor *lldp_neighbor_unlink(sd_lldp_neighbor *n) { + + /* Removes the neighbor object from the LLDP object, and frees it if it also has no other reference. */ + + if (!n) + return NULL; + + if (!n->lldp_rx) + return NULL; + + /* Only remove the neighbor object from the hash table if it's in there, don't complain if it isn't. This is + * because we are used as destructor call for hashmap_clear() and thus sometimes are called to de-register + * ourselves from the hashtable and sometimes are called after we already are de-registered. */ + + (void) hashmap_remove_value(n->lldp_rx->neighbor_by_id, &n->id, n); + + assert_se(prioq_remove(n->lldp_rx->neighbor_by_expiry, n, &n->prioq_idx) >= 0); + + n->lldp_rx = NULL; + + if (n->n_ref <= 0) + lldp_neighbor_free(n); + + return NULL; +} + +sd_lldp_neighbor *lldp_neighbor_new(size_t raw_size) { + sd_lldp_neighbor *n; + + if (raw_size > SIZE_MAX - ALIGN(sizeof(sd_lldp_neighbor))) + return NULL; + + n = malloc0(ALIGN(sizeof(sd_lldp_neighbor)) + raw_size); + if (!n) + return NULL; + + n->raw_size = raw_size; + n->n_ref = 1; + + return n; +} + +static int parse_string(sd_lldp_rx *lldp_rx, char **s, const void *q, size_t n) { + const char *p = q; + char *k; + + assert(s); + assert(p || n == 0); + + if (*s) { + log_lldp_rx(lldp_rx, "Found duplicate string, ignoring field."); + return 0; + } + + /* Strip trailing NULs, just to be nice */ + while (n > 0 && p[n-1] == 0) + n--; + + if (n <= 0) /* Ignore empty strings */ + return 0; + + /* Look for inner NULs */ + if (memchr(p, 0, n)) { + log_lldp_rx(lldp_rx, "Found inner NUL in string, ignoring field."); + return 0; + } + + /* Let's escape weird chars, for security reasons */ + k = cescape_length(p, n); + if (!k) + return log_oom_debug(); + + free_and_replace(*s, k); + + return 1; +} + +int lldp_neighbor_parse(sd_lldp_neighbor *n) { + struct ether_header h; + const uint8_t *p; + size_t left; + int r; + + assert(n); + + if (n->raw_size < sizeof(struct ether_header)) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Received truncated packet, ignoring."); + + memcpy(&h, LLDP_NEIGHBOR_RAW(n), sizeof(h)); + + if (h.ether_type != htobe16(ETHERTYPE_LLDP)) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Received packet with wrong type, ignoring."); + + if (h.ether_dhost[0] != 0x01 || + h.ether_dhost[1] != 0x80 || + h.ether_dhost[2] != 0xc2 || + h.ether_dhost[3] != 0x00 || + h.ether_dhost[4] != 0x00 || + !IN_SET(h.ether_dhost[5], 0x00, 0x03, 0x0e)) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Received packet with wrong destination address, ignoring."); + + memcpy(&n->source_address, h.ether_shost, sizeof(struct ether_addr)); + memcpy(&n->destination_address, h.ether_dhost, sizeof(struct ether_addr)); + + p = (const uint8_t*) LLDP_NEIGHBOR_RAW(n) + sizeof(struct ether_header); + left = n->raw_size - sizeof(struct ether_header); + + for (;;) { + uint8_t type; + uint16_t length; + + if (left < 2) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "TLV lacks header, ignoring."); + + type = p[0] >> 1; + length = p[1] + (((uint16_t) (p[0] & 1)) << 8); + p += 2, left -= 2; + + if (left < length) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "TLV truncated, ignoring datagram."); + + switch (type) { + + case SD_LLDP_TYPE_END: + if (length != 0) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "End marker TLV not zero-sized, ignoring datagram."); + + /* Note that after processing the SD_LLDP_TYPE_END left could still be > 0 + * as the message may contain padding (see IEEE 802.1AB-2016, sec. 8.5.12) */ + + goto end_marker; + + case SD_LLDP_TYPE_CHASSIS_ID: + if (length < 2 || length > 256) + /* includes the chassis subtype, hence one extra byte */ + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Chassis ID field size out of range, ignoring datagram."); + + if (n->id.chassis_id) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Duplicate chassis ID field, ignoring datagram."); + + n->id.chassis_id = memdup(p, length); + if (!n->id.chassis_id) + return log_oom_debug(); + + n->id.chassis_id_size = length; + break; + + case SD_LLDP_TYPE_PORT_ID: + if (length < 2 || length > 256) + /* includes the port subtype, hence one extra byte */ + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Port ID field size out of range, ignoring datagram."); + + if (n->id.port_id) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Duplicate port ID field, ignoring datagram."); + + n->id.port_id = memdup(p, length); + if (!n->id.port_id) + return log_oom_debug(); + + n->id.port_id_size = length; + break; + + case SD_LLDP_TYPE_TTL: + if (length != 2) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "TTL field has wrong size, ignoring datagram."); + + if (n->has_ttl) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Duplicate TTL field, ignoring datagram."); + + n->ttl = unaligned_read_be16(p); + n->has_ttl = true; + break; + + case SD_LLDP_TYPE_PORT_DESCRIPTION: + r = parse_string(n->lldp_rx, &n->port_description, p, length); + if (r < 0) + return r; + break; + + case SD_LLDP_TYPE_SYSTEM_NAME: + r = parse_string(n->lldp_rx, &n->system_name, p, length); + if (r < 0) + return r; + break; + + case SD_LLDP_TYPE_SYSTEM_DESCRIPTION: + r = parse_string(n->lldp_rx, &n->system_description, p, length); + if (r < 0) + return r; + break; + + case SD_LLDP_TYPE_SYSTEM_CAPABILITIES: + if (length != 4) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "System capabilities field has wrong size."); + + n->system_capabilities = unaligned_read_be16(p); + n->enabled_capabilities = unaligned_read_be16(p + 2); + n->has_capabilities = true; + break; + + case SD_LLDP_TYPE_PRIVATE: + if (length < 4) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "Found private TLV that is too short, ignoring."); + + /* RFC 8520: MUD URL */ + if (memcmp(p, SD_LLDP_OUI_IANA_MUD, sizeof(SD_LLDP_OUI_IANA_MUD)) == 0) { + r = parse_string(n->lldp_rx, &n->mud_url, p + sizeof(SD_LLDP_OUI_IANA_MUD), + length - sizeof(SD_LLDP_OUI_IANA_MUD)); + if (r < 0) + return r; + } + break; + } + + p += length, left -= length; + } + +end_marker: + if (!n->id.chassis_id || !n->id.port_id || !n->has_ttl) + return log_lldp_rx_errno(n->lldp_rx, SYNTHETIC_ERRNO(EBADMSG), + "One or more mandatory TLV missing in datagram. Ignoring."); + + n->rindex = sizeof(struct ether_header); + + return 0; +} + +void lldp_neighbor_start_ttl(sd_lldp_neighbor *n) { + assert(n); + + if (n->ttl > 0) { + usec_t base; + + /* Use the packet's timestamp if there is one known */ + base = triple_timestamp_by_clock(&n->timestamp, CLOCK_BOOTTIME); + if (!timestamp_is_set(base)) + base = now(CLOCK_BOOTTIME); /* Otherwise, take the current time */ + + n->until = usec_add(base, n->ttl * USEC_PER_SEC); + } else + n->until = 0; + + if (n->lldp_rx) + prioq_reshuffle(n->lldp_rx->neighbor_by_expiry, n, &n->prioq_idx); +} + +bool lldp_neighbor_equal(const sd_lldp_neighbor *a, const sd_lldp_neighbor *b) { + if (a == b) + return true; + + if (!a || !b) + return false; + + if (a->raw_size != b->raw_size) + return false; + + return memcmp(LLDP_NEIGHBOR_RAW(a), LLDP_NEIGHBOR_RAW(b), a->raw_size) == 0; +} + +int sd_lldp_neighbor_get_source_address(sd_lldp_neighbor *n, struct ether_addr* address) { + assert_return(n, -EINVAL); + assert_return(address, -EINVAL); + + *address = n->source_address; + return 0; +} + +int sd_lldp_neighbor_get_destination_address(sd_lldp_neighbor *n, struct ether_addr* address) { + assert_return(n, -EINVAL); + assert_return(address, -EINVAL); + + *address = n->destination_address; + return 0; +} + +int sd_lldp_neighbor_get_raw(sd_lldp_neighbor *n, const void **ret, size_t *size) { + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + assert_return(size, -EINVAL); + + *ret = LLDP_NEIGHBOR_RAW(n); + *size = n->raw_size; + + return 0; +} + +int sd_lldp_neighbor_get_chassis_id(sd_lldp_neighbor *n, uint8_t *type, const void **ret, size_t *size) { + assert_return(n, -EINVAL); + assert_return(type, -EINVAL); + assert_return(ret, -EINVAL); + assert_return(size, -EINVAL); + + assert(n->id.chassis_id_size > 0); + + *type = *(uint8_t*) n->id.chassis_id; + *ret = (uint8_t*) n->id.chassis_id + 1; + *size = n->id.chassis_id_size - 1; + + return 0; +} + +static int format_mac_address(const void *data, size_t sz, char **ret) { + struct ether_addr a; + char *k; + + assert(data || sz <= 0); + + if (sz != 7) + return 0; + + memcpy(&a, (uint8_t*) data + 1, sizeof(a)); + + k = new(char, ETHER_ADDR_TO_STRING_MAX); + if (!k) + return -ENOMEM; + + *ret = ether_addr_to_string(&a, k); + return 1; +} + +static int format_network_address(const void *data, size_t sz, char **ret) { + union in_addr_union a; + int family, r; + + if (sz == 6 && ((uint8_t*) data)[1] == 1) { + memcpy(&a.in, (uint8_t*) data + 2, sizeof(a.in)); + family = AF_INET; + } else if (sz == 18 && ((uint8_t*) data)[1] == 2) { + memcpy(&a.in6, (uint8_t*) data + 2, sizeof(a.in6)); + family = AF_INET6; + } else + return 0; + + r = in_addr_to_string(family, &a, ret); + if (r < 0) + return r; + return 1; +} + +int sd_lldp_neighbor_get_chassis_id_as_string(sd_lldp_neighbor *n, const char **ret) { + char *k; + int r; + + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (n->chassis_id_as_string) { + *ret = n->chassis_id_as_string; + return 0; + } + + assert(n->id.chassis_id_size > 0); + + switch (*(uint8_t*) n->id.chassis_id) { + + case SD_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT: + case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS: + case SD_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT: + case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME: + case SD_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED: + k = cescape_length((char*) n->id.chassis_id + 1, n->id.chassis_id_size - 1); + if (!k) + return -ENOMEM; + + goto done; + + case SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS: + r = format_mac_address(n->id.chassis_id, n->id.chassis_id_size, &k); + if (r < 0) + return r; + if (r > 0) + goto done; + + break; + + case SD_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS: + r = format_network_address(n->id.chassis_id, n->id.chassis_id_size, &k); + if (r < 0) + return r; + if (r > 0) + goto done; + + break; + } + + /* Generic fallback */ + k = hexmem(n->id.chassis_id, n->id.chassis_id_size); + if (!k) + return -ENOMEM; + +done: + *ret = n->chassis_id_as_string = k; + return 0; +} + +int sd_lldp_neighbor_get_port_id(sd_lldp_neighbor *n, uint8_t *type, const void **ret, size_t *size) { + assert_return(n, -EINVAL); + assert_return(type, -EINVAL); + assert_return(ret, -EINVAL); + assert_return(size, -EINVAL); + + assert(n->id.port_id_size > 0); + + *type = *(uint8_t*) n->id.port_id; + *ret = (uint8_t*) n->id.port_id + 1; + *size = n->id.port_id_size - 1; + + return 0; +} + +int sd_lldp_neighbor_get_port_id_as_string(sd_lldp_neighbor *n, const char **ret) { + char *k; + int r; + + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (n->port_id_as_string) { + *ret = n->port_id_as_string; + return 0; + } + + assert(n->id.port_id_size > 0); + + switch (*(uint8_t*) n->id.port_id) { + + case SD_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS: + case SD_LLDP_PORT_SUBTYPE_PORT_COMPONENT: + case SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME: + case SD_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED: + k = cescape_length((char*) n->id.port_id + 1, n->id.port_id_size - 1); + if (!k) + return -ENOMEM; + + goto done; + + case SD_LLDP_PORT_SUBTYPE_MAC_ADDRESS: + r = format_mac_address(n->id.port_id, n->id.port_id_size, &k); + if (r < 0) + return r; + if (r > 0) + goto done; + + break; + + case SD_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS: + r = format_network_address(n->id.port_id, n->id.port_id_size, &k); + if (r < 0) + return r; + if (r > 0) + goto done; + + break; + } + + /* Generic fallback */ + k = hexmem(n->id.port_id, n->id.port_id_size); + if (!k) + return -ENOMEM; + +done: + *ret = n->port_id_as_string = k; + return 0; +} + +int sd_lldp_neighbor_get_ttl(sd_lldp_neighbor *n, uint16_t *ret_sec) { + assert_return(n, -EINVAL); + assert_return(ret_sec, -EINVAL); + + *ret_sec = n->ttl; + return 0; +} + +int sd_lldp_neighbor_get_system_name(sd_lldp_neighbor *n, const char **ret) { + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (!n->system_name) + return -ENODATA; + + *ret = n->system_name; + return 0; +} + +int sd_lldp_neighbor_get_system_description(sd_lldp_neighbor *n, const char **ret) { + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (!n->system_description) + return -ENODATA; + + *ret = n->system_description; + return 0; +} + +int sd_lldp_neighbor_get_port_description(sd_lldp_neighbor *n, const char **ret) { + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (!n->port_description) + return -ENODATA; + + *ret = n->port_description; + return 0; +} + +int sd_lldp_neighbor_get_mud_url(sd_lldp_neighbor *n, const char **ret) { + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (!n->mud_url) + return -ENODATA; + + *ret = n->mud_url; + return 0; +} + +int sd_lldp_neighbor_get_system_capabilities(sd_lldp_neighbor *n, uint16_t *ret) { + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (!n->has_capabilities) + return -ENODATA; + + *ret = n->system_capabilities; + return 0; +} + +int sd_lldp_neighbor_get_enabled_capabilities(sd_lldp_neighbor *n, uint16_t *ret) { + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + + if (!n->has_capabilities) + return -ENODATA; + + *ret = n->enabled_capabilities; + return 0; +} + +int sd_lldp_neighbor_from_raw(sd_lldp_neighbor **ret, const void *raw, size_t raw_size) { + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL; + int r; + + assert_return(ret, -EINVAL); + assert_return(raw || raw_size <= 0, -EINVAL); + + n = lldp_neighbor_new(raw_size); + if (!n) + return -ENOMEM; + + memcpy_safe(LLDP_NEIGHBOR_RAW(n), raw, raw_size); + + r = lldp_neighbor_parse(n); + if (r < 0) + return r; + + *ret = TAKE_PTR(n); + + return r; +} + +int sd_lldp_neighbor_tlv_rewind(sd_lldp_neighbor *n) { + assert_return(n, -EINVAL); + + assert(n->raw_size >= sizeof(struct ether_header)); + n->rindex = sizeof(struct ether_header); + + return n->rindex < n->raw_size; +} + +int sd_lldp_neighbor_tlv_next(sd_lldp_neighbor *n) { + size_t length; + + assert_return(n, -EINVAL); + + if (n->rindex == n->raw_size) /* EOF */ + return -ESPIPE; + + if (n->rindex + 2 > n->raw_size) /* Truncated message */ + return -EBADMSG; + + length = LLDP_NEIGHBOR_TLV_LENGTH(n); + if (n->rindex + 2 + length > n->raw_size) + return -EBADMSG; + + n->rindex += 2 + length; + return n->rindex < n->raw_size; +} + +int sd_lldp_neighbor_tlv_get_type(sd_lldp_neighbor *n, uint8_t *type) { + assert_return(n, -EINVAL); + assert_return(type, -EINVAL); + + if (n->rindex == n->raw_size) /* EOF */ + return -ESPIPE; + + if (n->rindex + 2 > n->raw_size) + return -EBADMSG; + + *type = LLDP_NEIGHBOR_TLV_TYPE(n); + return 0; +} + +int sd_lldp_neighbor_tlv_is_type(sd_lldp_neighbor *n, uint8_t type) { + uint8_t k; + int r; + + assert_return(n, -EINVAL); + + r = sd_lldp_neighbor_tlv_get_type(n, &k); + if (r < 0) + return r; + + return type == k; +} + +int sd_lldp_neighbor_tlv_get_oui(sd_lldp_neighbor *n, uint8_t oui[_SD_ARRAY_STATIC 3], uint8_t *subtype) { + const uint8_t *d; + size_t length; + int r; + + assert_return(n, -EINVAL); + assert_return(oui, -EINVAL); + assert_return(subtype, -EINVAL); + + r = sd_lldp_neighbor_tlv_is_type(n, SD_LLDP_TYPE_PRIVATE); + if (r < 0) + return r; + if (r == 0) + return -ENXIO; + + length = LLDP_NEIGHBOR_TLV_LENGTH(n); + if (length < 4) + return -EBADMSG; + + if (n->rindex + 2 + length > n->raw_size) + return -EBADMSG; + + d = LLDP_NEIGHBOR_TLV_DATA(n); + memcpy(oui, d, 3); + *subtype = d[3]; + + return 0; +} + +int sd_lldp_neighbor_tlv_is_oui(sd_lldp_neighbor *n, const uint8_t oui[_SD_ARRAY_STATIC 3], uint8_t subtype) { + uint8_t k[3], st; + int r; + + r = sd_lldp_neighbor_tlv_get_oui(n, k, &st); + if (r == -ENXIO) + return 0; + if (r < 0) + return r; + + return memcmp(k, oui, 3) == 0 && st == subtype; +} + +int sd_lldp_neighbor_tlv_get_raw(sd_lldp_neighbor *n, const void **ret, size_t *size) { + size_t length; + + assert_return(n, -EINVAL); + assert_return(ret, -EINVAL); + assert_return(size, -EINVAL); + + /* Note that this returns the full TLV, including the TLV header */ + + if (n->rindex + 2 > n->raw_size) + return -EBADMSG; + + length = LLDP_NEIGHBOR_TLV_LENGTH(n); + if (n->rindex + 2 + length > n->raw_size) + return -EBADMSG; + + *ret = (uint8_t*) LLDP_NEIGHBOR_RAW(n) + n->rindex; + *size = length + 2; + + return 0; +} + +int sd_lldp_neighbor_get_timestamp(sd_lldp_neighbor *n, clockid_t clock, uint64_t *ret) { + assert_return(n, -EINVAL); + assert_return(TRIPLE_TIMESTAMP_HAS_CLOCK(clock), -EOPNOTSUPP); + assert_return(clock_supported(clock), -EOPNOTSUPP); + assert_return(ret, -EINVAL); + + if (!triple_timestamp_is_set(&n->timestamp)) + return -ENODATA; + + *ret = triple_timestamp_by_clock(&n->timestamp, clock); + return 0; +} diff --git a/src/libsystemd-network/lldp-neighbor.h b/src/libsystemd-network/lldp-neighbor.h new file mode 100644 index 0000000..016286b --- /dev/null +++ b/src/libsystemd-network/lldp-neighbor.h @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-lldp-rx.h" + +#include "hash-funcs.h" +#include "lldp-rx-internal.h" +#include "time-util.h" + +typedef struct LLDPNeighborID { + /* The spec calls this an "MSAP identifier" */ + void *chassis_id; + size_t chassis_id_size; + + void *port_id; + size_t port_id_size; +} LLDPNeighborID; + +struct sd_lldp_neighbor { + /* Neighbor objects stay around as long as they are linked into an "sd_lldp_rx" object or n_ref > 0. */ + sd_lldp_rx *lldp_rx; + unsigned n_ref; + + triple_timestamp timestamp; + + usec_t until; + unsigned prioq_idx; + + struct ether_addr source_address; + struct ether_addr destination_address; + + LLDPNeighborID id; + + /* The raw packet size. The data is appended to the object, accessible via LLDP_NEIGHBOR_RAW() */ + size_t raw_size; + + /* The current read index for the iterative TLV interface */ + size_t rindex; + + /* And a couple of fields parsed out. */ + bool has_ttl:1; + bool has_capabilities:1; + bool has_port_vlan_id:1; + + uint16_t ttl; + + uint16_t system_capabilities; + uint16_t enabled_capabilities; + + char *port_description; + char *system_name; + char *system_description; + char *mud_url; + + uint16_t port_vlan_id; + + char *chassis_id_as_string; + char *port_id_as_string; +}; + +static inline void *LLDP_NEIGHBOR_RAW(const sd_lldp_neighbor *n) { + return (uint8_t*) n + ALIGN(sizeof(sd_lldp_neighbor)); +} + +static inline uint8_t LLDP_NEIGHBOR_TLV_TYPE(const sd_lldp_neighbor *n) { + return ((uint8_t*) LLDP_NEIGHBOR_RAW(n))[n->rindex] >> 1; +} + +static inline size_t LLDP_NEIGHBOR_TLV_LENGTH(const sd_lldp_neighbor *n) { + uint8_t *p; + + p = (uint8_t*) LLDP_NEIGHBOR_RAW(n) + n->rindex; + return p[1] + (((size_t) (p[0] & 1)) << 8); +} + +static inline void* LLDP_NEIGHBOR_TLV_DATA(const sd_lldp_neighbor *n) { + return ((uint8_t*) LLDP_NEIGHBOR_RAW(n)) + n->rindex + 2; +} + +extern const struct hash_ops lldp_neighbor_hash_ops; +int lldp_neighbor_id_compare_func(const LLDPNeighborID *x, const LLDPNeighborID *y); +int lldp_neighbor_prioq_compare_func(const void *a, const void *b); + +sd_lldp_neighbor *lldp_neighbor_unlink(sd_lldp_neighbor *n); +sd_lldp_neighbor *lldp_neighbor_new(size_t raw_size); +int lldp_neighbor_parse(sd_lldp_neighbor *n); +void lldp_neighbor_start_ttl(sd_lldp_neighbor *n); +bool lldp_neighbor_equal(const sd_lldp_neighbor *a, const sd_lldp_neighbor *b); diff --git a/src/libsystemd-network/lldp-network.c b/src/libsystemd-network/lldp-network.c new file mode 100644 index 0000000..598669f --- /dev/null +++ b/src/libsystemd-network/lldp-network.c @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/filter.h> +#include <netinet/if_ether.h> + +#include "fd-util.h" +#include "lldp-network.h" +#include "missing_network.h" +#include "socket-util.h" + +int lldp_network_bind_raw_socket(int ifindex) { + static const struct sock_filter filter[] = { + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct ethhdr, h_dest)), /* A <- 4 bytes of destination MAC */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x0180c200, 1, 0), /* A != 01:80:c2:00 */ + BPF_STMT(BPF_RET + BPF_K, 0), /* drop packet */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct ethhdr, h_dest) + 4), /* A <- remaining 2 bytes of destination MAC */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x0000, 3, 0), /* A != 00:00 */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x0003, 2, 0), /* A != 00:03 */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x000e, 1, 0), /* A != 00:0e */ + BPF_STMT(BPF_RET + BPF_K, 0), /* drop packet */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct ethhdr, h_proto)), /* A <- protocol */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETHERTYPE_LLDP, 1, 0), /* A != ETHERTYPE_LLDP */ + BPF_STMT(BPF_RET + BPF_K, 0), /* drop packet */ + BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept packet */ + }; + static const struct sock_fprog fprog = { + .len = ELEMENTSOF(filter), + .filter = (struct sock_filter*) filter, + }; + struct packet_mreq mreq = { + .mr_ifindex = ifindex, + .mr_type = PACKET_MR_MULTICAST, + .mr_alen = ETH_ALEN, + .mr_address = { 0x01, 0x80, 0xC2, 0x00, 0x00, 0x00 } + }; + union sockaddr_union saddrll = { + .ll.sll_family = AF_PACKET, + .ll.sll_ifindex = ifindex, + }; + _cleanup_close_ int fd = -EBADF; + + assert(ifindex > 0); + + fd = socket(AF_PACKET, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, + htobe16(ETHERTYPE_LLDP)); + if (fd < 0) + return -errno; + + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)) < 0) + return -errno; + + /* customer bridge */ + if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) + return -errno; + + /* non TPMR bridge */ + mreq.mr_address[ETH_ALEN - 1] = 0x03; + if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) + return -errno; + + /* nearest bridge */ + mreq.mr_address[ETH_ALEN - 1] = 0x0E; + if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) + return -errno; + + if (bind(fd, &saddrll.sa, sizeof(saddrll.ll)) < 0) + return -errno; + + return TAKE_FD(fd); +} diff --git a/src/libsystemd-network/lldp-network.h b/src/libsystemd-network/lldp-network.h new file mode 100644 index 0000000..bc69b32 --- /dev/null +++ b/src/libsystemd-network/lldp-network.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-event.h" + +int lldp_network_bind_raw_socket(int ifindex); diff --git a/src/libsystemd-network/lldp-rx-internal.h b/src/libsystemd-network/lldp-rx-internal.h new file mode 100644 index 0000000..83d0bc4 --- /dev/null +++ b/src/libsystemd-network/lldp-rx-internal.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-event.h" +#include "sd-lldp-rx.h" + +#include "hashmap.h" +#include "network-common.h" +#include "prioq.h" + +struct sd_lldp_rx { + unsigned n_ref; + + int ifindex; + char *ifname; + int fd; + + sd_event *event; + int64_t event_priority; + sd_event_source *io_event_source; + sd_event_source *timer_event_source; + + Prioq *neighbor_by_expiry; + Hashmap *neighbor_by_id; + + uint64_t neighbors_max; + + sd_lldp_rx_callback_t callback; + void *userdata; + + uint16_t capability_mask; + + struct ether_addr filter_address; +}; + +const char* lldp_rx_event_to_string(sd_lldp_rx_event_t e) _const_; +sd_lldp_rx_event_t lldp_rx_event_from_string(const char *s) _pure_; + +#define log_lldp_rx_errno(lldp_rx, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "LLDP Rx: ", \ + sd_lldp_rx, lldp_rx, \ + error, fmt, ##__VA_ARGS__) +#define log_lldp_rx(lldp_rx, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "LLDP Rx: ", \ + sd_lldp_rx, lldp_rx, \ + 0, fmt, ##__VA_ARGS__) diff --git a/src/libsystemd-network/meson.build b/src/libsystemd-network/meson.build new file mode 100644 index 0000000..93186e2 --- /dev/null +++ b/src/libsystemd-network/meson.build @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +sources = files( + 'arp-util.c', + 'dhcp-identifier.c', + 'dhcp-network.c', + 'dhcp-option.c', + 'dhcp-packet.c', + 'dhcp6-network.c', + 'dhcp6-option.c', + 'dhcp6-protocol.c', + 'icmp6-util.c', + 'lldp-neighbor.c', + 'lldp-network.c', + 'ndisc-protocol.c', + 'ndisc-router.c', + 'network-common.c', + 'network-internal.c', + 'sd-dhcp-client.c', + 'sd-dhcp-lease.c', + 'sd-dhcp-server.c', + 'sd-dhcp6-client.c', + 'sd-dhcp6-lease.c', + 'sd-ipv4acd.c', + 'sd-ipv4ll.c', + 'sd-lldp-rx.c', + 'sd-lldp-tx.c', + 'sd-ndisc.c', + 'sd-radv.c', +) + +libsystemd_network = static_library( + 'systemd-network', + sources, + include_directories : includes, + dependencies : userspace, + build_by_default : false) + +libsystemd_network_includes = [includes, include_directories('.')] + +############################################################ + +network_test_template = test_template + { + 'link_with' : [ + libshared, + libsystemd_network, + ], + 'suite' : 'network', +} + +network_fuzz_template = fuzz_template + { + 'link_with' : [ + libshared, + libsystemd_network, + ], +} + +executables += [ + network_test_template + { + 'sources' : files('test-acd.c'), + 'type' : 'manual', + }, + network_test_template + { + 'sources' : files('test-dhcp-client.c'), + }, + network_test_template + { + 'sources' : files('test-dhcp-option.c'), + }, + network_test_template + { + 'sources' : files('test-dhcp-server.c'), + }, + network_test_template + { + 'sources' : files('test-dhcp6-client.c'), + }, + network_test_template + { + 'sources' : files('test-ipv4ll-manual.c'), + 'type' : 'manual', + }, + network_test_template + { + 'sources' : files('test-ipv4ll.c'), + }, + network_test_template + { + 'sources' : files('test-lldp-rx.c'), + }, + network_test_template + { + 'sources' : files( + 'test-ndisc-ra.c', + 'icmp6-util-unix.c', + ), + }, + network_test_template + { + 'sources' : files( + 'test-ndisc-rs.c', + 'icmp6-util-unix.c', + ), + }, + network_test_template + { + 'sources' : files('test-sd-dhcp-lease.c'), + }, + network_fuzz_template + { + 'sources' : files('fuzz-dhcp-client.c'), + }, + network_fuzz_template + { + 'sources' : files('fuzz-dhcp6-client.c'), + }, + network_fuzz_template + { + 'sources' : files('fuzz-dhcp-server.c'), + }, + network_fuzz_template + { + 'sources' : files('fuzz-dhcp-server-relay.c'), + }, + network_fuzz_template + { + 'sources' : files('fuzz-lldp-rx.c'), + }, + network_fuzz_template + { + 'sources' : files( + 'fuzz-ndisc-rs.c', + 'icmp6-util-unix.c', + ), + }, +] diff --git a/src/libsystemd-network/ndisc-internal.h b/src/libsystemd-network/ndisc-internal.h new file mode 100644 index 0000000..615de0d --- /dev/null +++ b/src/libsystemd-network/ndisc-internal.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include "sd-ndisc.h" + +#include "network-common.h" +#include "time-util.h" + +#define NDISC_ROUTER_SOLICITATION_INTERVAL (4U * USEC_PER_SEC) +#define NDISC_MAX_ROUTER_SOLICITATION_INTERVAL (3600U * USEC_PER_SEC) +#define NDISC_MAX_ROUTER_SOLICITATIONS 3U + +struct sd_ndisc { + unsigned n_ref; + + int ifindex; + char *ifname; + int fd; + + sd_event *event; + int event_priority; + + struct ether_addr mac_addr; + + sd_event_source *recv_event_source; + sd_event_source *timeout_event_source; + sd_event_source *timeout_no_ra; + + usec_t retransmit_time; + + sd_ndisc_callback_t callback; + void *userdata; +}; + +const char* ndisc_event_to_string(sd_ndisc_event_t e) _const_; +sd_ndisc_event_t ndisc_event_from_string(const char *s) _pure_; + +#define log_ndisc_errno(ndisc, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "NDISC: ", \ + sd_ndisc, ndisc, \ + error, fmt, ##__VA_ARGS__) +#define log_ndisc(ndisc, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "NDISC: ", \ + sd_ndisc, ndisc, \ + 0, fmt, ##__VA_ARGS__) diff --git a/src/libsystemd-network/ndisc-protocol.c b/src/libsystemd-network/ndisc-protocol.c new file mode 100644 index 0000000..fae4a58 --- /dev/null +++ b/src/libsystemd-network/ndisc-protocol.c @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "ndisc-protocol.h" + +static const uint8_t prefix_length_code_to_prefix_length[_PREFIX_LENGTH_CODE_MAX] = { + [PREFIX_LENGTH_CODE_96] = 96, + [PREFIX_LENGTH_CODE_64] = 64, + [PREFIX_LENGTH_CODE_56] = 56, + [PREFIX_LENGTH_CODE_48] = 48, + [PREFIX_LENGTH_CODE_40] = 40, + [PREFIX_LENGTH_CODE_32] = 32, +}; + +int pref64_plc_to_prefix_length(uint16_t plc, uint8_t *ret) { + plc &= PREF64_PLC_MASK; + if (plc >= _PREFIX_LENGTH_CODE_MAX) + return -EINVAL; + + if (ret) + *ret = prefix_length_code_to_prefix_length[plc]; + return 0; +} + +int pref64_prefix_length_to_plc(uint8_t prefixlen, uint8_t *ret) { + assert(ret); + + for (size_t i = 0; i < ELEMENTSOF(prefix_length_code_to_prefix_length); i++) + if (prefix_length_code_to_prefix_length[i] == prefixlen) { + *ret = i; + return 0; + } + + return -EINVAL; +} diff --git a/src/libsystemd-network/ndisc-protocol.h b/src/libsystemd-network/ndisc-protocol.h new file mode 100644 index 0000000..8e403e3 --- /dev/null +++ b/src/libsystemd-network/ndisc-protocol.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "time-util.h" + +/* RFC 8781: PREF64 or (NAT64 prefix) */ +#define PREF64_SCALED_LIFETIME_MASK 0xfff8 +#define PREF64_PLC_MASK 0x0007 +#define PREF64_MAX_LIFETIME_USEC (65528 * USEC_PER_SEC) + +typedef enum PrefixLengthCode { + PREFIX_LENGTH_CODE_96, + PREFIX_LENGTH_CODE_64, + PREFIX_LENGTH_CODE_56, + PREFIX_LENGTH_CODE_48, + PREFIX_LENGTH_CODE_40, + PREFIX_LENGTH_CODE_32, + _PREFIX_LENGTH_CODE_MAX, + _PREFIX_LENGTH_CODE_INVALID = -EINVAL, +} PrefixLengthCode; + +/* rfc8781: section 4 - Scaled Lifetime: 13-bit unsigned integer. PREFIX_LEN (Prefix Length Code): 3-bit unsigned integer */ +struct nd_opt_prefix64_info { + uint8_t type; + uint8_t length; + uint16_t lifetime_and_plc; + uint8_t prefix[12]; +} __attribute__((__packed__)); + +int pref64_plc_to_prefix_length(uint16_t plc, uint8_t *ret); +int pref64_prefix_length_to_plc(uint8_t prefixlen, uint8_t *ret); diff --git a/src/libsystemd-network/ndisc-router.c b/src/libsystemd-network/ndisc-router.c new file mode 100644 index 0000000..5162df7 --- /dev/null +++ b/src/libsystemd-network/ndisc-router.c @@ -0,0 +1,913 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <netinet/icmp6.h> + +#include "sd-ndisc.h" + +#include "alloc-util.h" +#include "dns-domain.h" +#include "hostname-util.h" +#include "memory-util.h" +#include "missing_network.h" +#include "ndisc-internal.h" +#include "ndisc-protocol.h" +#include "ndisc-router.h" +#include "strv.h" + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_ndisc_router, sd_ndisc_router, mfree); + +sd_ndisc_router *ndisc_router_new(size_t raw_size) { + sd_ndisc_router *rt; + + if (raw_size > SIZE_MAX - ALIGN(sizeof(sd_ndisc_router))) + return NULL; + + rt = malloc0(ALIGN(sizeof(sd_ndisc_router)) + raw_size); + if (!rt) + return NULL; + + rt->raw_size = raw_size; + rt->n_ref = 1; + + return rt; +} + +int sd_ndisc_router_get_address(sd_ndisc_router *rt, struct in6_addr *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + if (in6_addr_is_null(&rt->address)) + return -ENODATA; + + *ret = rt->address; + return 0; +} + +int sd_ndisc_router_get_timestamp(sd_ndisc_router *rt, clockid_t clock, uint64_t *ret) { + assert_return(rt, -EINVAL); + assert_return(TRIPLE_TIMESTAMP_HAS_CLOCK(clock), -EOPNOTSUPP); + assert_return(clock_supported(clock), -EOPNOTSUPP); + assert_return(ret, -EINVAL); + + if (!triple_timestamp_is_set(&rt->timestamp)) + return -ENODATA; + + *ret = triple_timestamp_by_clock(&rt->timestamp, clock); + return 0; +} + +#define DEFINE_GET_TIMESTAMP(name) \ + int sd_ndisc_router_##name##_timestamp( \ + sd_ndisc_router *rt, \ + clockid_t clock, \ + uint64_t *ret) { \ + \ + usec_t s, t; \ + int r; \ + \ + assert_return(rt, -EINVAL); \ + assert_return(ret, -EINVAL); \ + \ + r = sd_ndisc_router_##name(rt, &s); \ + if (r < 0) \ + return r; \ + \ + r = sd_ndisc_router_get_timestamp(rt, clock, &t); \ + if (r < 0) \ + return r; \ + \ + *ret = time_span_to_stamp(s, t); \ + return 0; \ + } + +DEFINE_GET_TIMESTAMP(get_lifetime); +DEFINE_GET_TIMESTAMP(prefix_get_valid_lifetime); +DEFINE_GET_TIMESTAMP(prefix_get_preferred_lifetime); +DEFINE_GET_TIMESTAMP(route_get_lifetime); +DEFINE_GET_TIMESTAMP(rdnss_get_lifetime); +DEFINE_GET_TIMESTAMP(dnssl_get_lifetime); +DEFINE_GET_TIMESTAMP(prefix64_get_lifetime); + +int sd_ndisc_router_get_raw(sd_ndisc_router *rt, const void **ret, size_t *ret_size) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + assert_return(ret_size, -EINVAL); + + *ret = NDISC_ROUTER_RAW(rt); + *ret_size = rt->raw_size; + + return 0; +} + +static bool pref64_option_verify(const struct nd_opt_prefix64_info *p, size_t length) { + uint16_t lifetime_and_plc; + + assert(p); + + if (length != sizeof(struct nd_opt_prefix64_info)) + return false; + + lifetime_and_plc = be16toh(p->lifetime_and_plc); + if (pref64_plc_to_prefix_length(lifetime_and_plc, NULL) < 0) + return false; + + return true; +} + +int ndisc_router_parse(sd_ndisc *nd, sd_ndisc_router *rt) { + struct nd_router_advert *a; + const uint8_t *p; + bool has_mtu = false, has_flag_extension = false; + size_t left; + + assert(rt); + + if (rt->raw_size < sizeof(struct nd_router_advert)) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Too small to be a router advertisement, ignoring."); + + /* Router advertisement packets are neatly aligned to 64-bit boundaries, hence we can access them directly */ + a = NDISC_ROUTER_RAW(rt); + + if (a->nd_ra_type != ND_ROUTER_ADVERT) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Received ND packet that is not a router advertisement, ignoring."); + + if (a->nd_ra_code != 0) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Received ND packet with wrong RA code, ignoring."); + + rt->hop_limit = a->nd_ra_curhoplimit; + rt->flags = a->nd_ra_flags_reserved; /* the first 8 bits */ + rt->lifetime_usec = be16_sec_to_usec(a->nd_ra_router_lifetime, /* max_as_infinity = */ false); + rt->icmp6_ratelimit_usec = be32_msec_to_usec(a->nd_ra_retransmit, /* max_as_infinity = */ false); + + rt->preference = (rt->flags >> 3) & 3; + if (!IN_SET(rt->preference, SD_NDISC_PREFERENCE_LOW, SD_NDISC_PREFERENCE_HIGH)) + rt->preference = SD_NDISC_PREFERENCE_MEDIUM; + + p = (const uint8_t*) NDISC_ROUTER_RAW(rt) + sizeof(struct nd_router_advert); + left = rt->raw_size - sizeof(struct nd_router_advert); + + for (;;) { + uint8_t type; + size_t length; + + if (left == 0) + break; + + if (left < 2) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Option lacks header, ignoring datagram."); + + type = p[0]; + length = p[1] * 8; + + if (length == 0) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Zero-length option, ignoring datagram."); + if (left < length) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Option truncated, ignoring datagram."); + + switch (type) { + + case SD_NDISC_OPTION_PREFIX_INFORMATION: + + if (length != 4*8) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Prefix option of invalid size, ignoring datagram."); + + if (p[2] > 128) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Bad prefix length, ignoring datagram."); + + break; + + case SD_NDISC_OPTION_MTU: { + uint32_t m; + + if (has_mtu) { + log_ndisc(nd, "MTU option specified twice, ignoring."); + break; + } + + if (length != 8) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "MTU option of invalid size, ignoring datagram."); + + m = be32toh(*(uint32_t*) (p + 4)); + if (m >= IPV6_MIN_MTU) /* ignore invalidly small MTUs */ + rt->mtu = m; + + has_mtu = true; + break; + } + + case SD_NDISC_OPTION_ROUTE_INFORMATION: + if (length < 1*8 || length > 3*8) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Route information option of invalid size, ignoring datagram."); + + if (p[2] > 128) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Bad route prefix length, ignoring datagram."); + + break; + + case SD_NDISC_OPTION_RDNSS: + if (length < 3*8 || (length % (2*8)) != 1*8) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), "RDNSS option has invalid size."); + + break; + + case SD_NDISC_OPTION_FLAGS_EXTENSION: + + if (has_flag_extension) { + log_ndisc(nd, "Flags extension option specified twice, ignoring."); + break; + } + + if (length < 1*8) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "Flags extension option has invalid size."); + + /* Add in the additional flags bits */ + rt->flags |= + ((uint64_t) p[2] << 8) | + ((uint64_t) p[3] << 16) | + ((uint64_t) p[4] << 24) | + ((uint64_t) p[5] << 32) | + ((uint64_t) p[6] << 40) | + ((uint64_t) p[7] << 48); + + has_flag_extension = true; + break; + + case SD_NDISC_OPTION_DNSSL: + if (length < 2*8) + return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "DNSSL option has invalid size."); + + break; + case SD_NDISC_OPTION_PREF64: { + if (!pref64_option_verify((struct nd_opt_prefix64_info *) p, length)) + log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), + "PREF64 prefix has invalid prefix length."); + break; + }} + + p += length, left -= length; + } + + rt->rindex = sizeof(struct nd_router_advert); + return 0; +} + +int sd_ndisc_router_get_hop_limit(sd_ndisc_router *rt, uint8_t *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + *ret = rt->hop_limit; + return 0; +} + +int sd_ndisc_router_get_icmp6_ratelimit(sd_ndisc_router *rt, uint64_t *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + *ret = rt->icmp6_ratelimit_usec; + return 0; +} + +int sd_ndisc_router_get_flags(sd_ndisc_router *rt, uint64_t *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + *ret = rt->flags; + return 0; +} + +int sd_ndisc_router_get_lifetime(sd_ndisc_router *rt, uint64_t *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + *ret = rt->lifetime_usec; + return 0; +} + +int sd_ndisc_router_get_preference(sd_ndisc_router *rt, unsigned *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + *ret = rt->preference; + return 0; +} + +int sd_ndisc_router_get_mtu(sd_ndisc_router *rt, uint32_t *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + if (rt->mtu <= 0) + return -ENODATA; + + *ret = rt->mtu; + return 0; +} + +int sd_ndisc_router_option_rewind(sd_ndisc_router *rt) { + assert_return(rt, -EINVAL); + + assert(rt->raw_size >= sizeof(struct nd_router_advert)); + rt->rindex = sizeof(struct nd_router_advert); + + return rt->rindex < rt->raw_size; +} + +int sd_ndisc_router_option_next(sd_ndisc_router *rt) { + size_t length; + + assert_return(rt, -EINVAL); + + if (rt->rindex == rt->raw_size) /* EOF */ + return -ESPIPE; + + if (rt->rindex + 2 > rt->raw_size) /* Truncated message */ + return -EBADMSG; + + length = NDISC_ROUTER_OPTION_LENGTH(rt); + if (rt->rindex + length > rt->raw_size) + return -EBADMSG; + + rt->rindex += length; + return rt->rindex < rt->raw_size; +} + +int sd_ndisc_router_option_get_type(sd_ndisc_router *rt, uint8_t *ret) { + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + if (rt->rindex == rt->raw_size) /* EOF */ + return -ESPIPE; + + if (rt->rindex + 2 > rt->raw_size) /* Truncated message */ + return -EBADMSG; + + *ret = NDISC_ROUTER_OPTION_TYPE(rt); + return 0; +} + +int sd_ndisc_router_option_is_type(sd_ndisc_router *rt, uint8_t type) { + uint8_t k; + int r; + + assert_return(rt, -EINVAL); + + r = sd_ndisc_router_option_get_type(rt, &k); + if (r < 0) + return r; + + return type == k; +} + +int sd_ndisc_router_option_get_raw(sd_ndisc_router *rt, const void **ret, size_t *ret_size) { + size_t length; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + assert_return(ret_size, -EINVAL); + + /* Note that this returns the full option, including the option header */ + + if (rt->rindex + 2 > rt->raw_size) + return -EBADMSG; + + length = NDISC_ROUTER_OPTION_LENGTH(rt); + if (rt->rindex + length > rt->raw_size) + return -EBADMSG; + + *ret = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex; + *ret_size = length; + + return 0; +} + +static int get_prefix_info(sd_ndisc_router *rt, struct nd_opt_prefix_info **ret) { + struct nd_opt_prefix_info *ri; + size_t length; + int r; + + assert(rt); + assert(ret); + + r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_PREFIX_INFORMATION); + if (r < 0) + return r; + if (r == 0) + return -EMEDIUMTYPE; + + length = NDISC_ROUTER_OPTION_LENGTH(rt); + if (length != sizeof(struct nd_opt_prefix_info)) + return -EBADMSG; + + ri = (struct nd_opt_prefix_info*) ((uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex); + if (ri->nd_opt_pi_prefix_len > 128) + return -EBADMSG; + + *ret = ri; + return 0; +} + +int sd_ndisc_router_prefix_get_valid_lifetime(sd_ndisc_router *rt, uint64_t *ret) { + struct nd_opt_prefix_info *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_prefix_info(rt, &ri); + if (r < 0) + return r; + + *ret = be32_sec_to_usec(ri->nd_opt_pi_valid_time, /* max_as_infinity = */ true); + return 0; +} + +int sd_ndisc_router_prefix_get_preferred_lifetime(sd_ndisc_router *rt, uint64_t *ret) { + struct nd_opt_prefix_info *pi; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_prefix_info(rt, &pi); + if (r < 0) + return r; + + *ret = be32_sec_to_usec(pi->nd_opt_pi_preferred_time, /* max_as_infinity = */ true); + return 0; +} + +int sd_ndisc_router_prefix_get_flags(sd_ndisc_router *rt, uint8_t *ret) { + struct nd_opt_prefix_info *pi; + uint8_t flags; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_prefix_info(rt, &pi); + if (r < 0) + return r; + + flags = pi->nd_opt_pi_flags_reserved; + + if ((flags & ND_OPT_PI_FLAG_AUTO) && (pi->nd_opt_pi_prefix_len != 64)) { + log_ndisc(NULL, "Invalid prefix length, ignoring prefix for stateless autoconfiguration."); + flags &= ~ND_OPT_PI_FLAG_AUTO; + } + + *ret = flags; + return 0; +} + +int sd_ndisc_router_prefix_get_address(sd_ndisc_router *rt, struct in6_addr *ret) { + struct nd_opt_prefix_info *pi; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_prefix_info(rt, &pi); + if (r < 0) + return r; + + *ret = pi->nd_opt_pi_prefix; + return 0; +} + +int sd_ndisc_router_prefix_get_prefixlen(sd_ndisc_router *rt, unsigned *ret) { + struct nd_opt_prefix_info *pi; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_prefix_info(rt, &pi); + if (r < 0) + return r; + + if (pi->nd_opt_pi_prefix_len > 128) + return -EBADMSG; + + *ret = pi->nd_opt_pi_prefix_len; + return 0; +} + +static int get_route_info(sd_ndisc_router *rt, uint8_t **ret) { + uint8_t *ri; + size_t length; + int r; + + assert(rt); + assert(ret); + + r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_ROUTE_INFORMATION); + if (r < 0) + return r; + if (r == 0) + return -EMEDIUMTYPE; + + length = NDISC_ROUTER_OPTION_LENGTH(rt); + if (length < 1*8 || length > 3*8) + return -EBADMSG; + + ri = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex; + + if (ri[2] > 128) + return -EBADMSG; + + *ret = ri; + return 0; +} + +int sd_ndisc_router_route_get_lifetime(sd_ndisc_router *rt, uint64_t *ret) { + uint8_t *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_route_info(rt, &ri); + if (r < 0) + return r; + + *ret = unaligned_be32_sec_to_usec(ri + 4, /* max_as_infinity = */ true); + return 0; +} + +int sd_ndisc_router_route_get_address(sd_ndisc_router *rt, struct in6_addr *ret) { + uint8_t *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_route_info(rt, &ri); + if (r < 0) + return r; + + zero(*ret); + memcpy(ret, ri + 8, NDISC_ROUTER_OPTION_LENGTH(rt) - 8); + + return 0; +} + +int sd_ndisc_router_route_get_prefixlen(sd_ndisc_router *rt, unsigned *ret) { + uint8_t *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_route_info(rt, &ri); + if (r < 0) + return r; + + *ret = ri[2]; + return 0; +} + +int sd_ndisc_router_route_get_preference(sd_ndisc_router *rt, unsigned *ret) { + uint8_t *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_route_info(rt, &ri); + if (r < 0) + return r; + + if (!IN_SET((ri[3] >> 3) & 3, SD_NDISC_PREFERENCE_LOW, SD_NDISC_PREFERENCE_MEDIUM, SD_NDISC_PREFERENCE_HIGH)) + return -EOPNOTSUPP; + + *ret = (ri[3] >> 3) & 3; + return 0; +} + +static int get_rdnss_info(sd_ndisc_router *rt, uint8_t **ret) { + size_t length; + int r; + + assert(rt); + assert(ret); + + r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_RDNSS); + if (r < 0) + return r; + if (r == 0) + return -EMEDIUMTYPE; + + length = NDISC_ROUTER_OPTION_LENGTH(rt); + if (length < 3*8 || (length % (2*8)) != 1*8) + return -EBADMSG; + + *ret = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex; + return 0; +} + +int sd_ndisc_router_rdnss_get_addresses(sd_ndisc_router *rt, const struct in6_addr **ret) { + uint8_t *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_rdnss_info(rt, &ri); + if (r < 0) + return r; + + *ret = (const struct in6_addr*) (ri + 8); + return (NDISC_ROUTER_OPTION_LENGTH(rt) - 8) / 16; +} + +int sd_ndisc_router_rdnss_get_lifetime(sd_ndisc_router *rt, uint64_t *ret) { + uint8_t *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_rdnss_info(rt, &ri); + if (r < 0) + return r; + + *ret = unaligned_be32_sec_to_usec(ri + 4, /* max_as_infinity = */ true); + return 0; +} + +static int get_dnssl_info(sd_ndisc_router *rt, uint8_t **ret) { + size_t length; + int r; + + assert(rt); + assert(ret); + + r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_DNSSL); + if (r < 0) + return r; + if (r == 0) + return -EMEDIUMTYPE; + + length = NDISC_ROUTER_OPTION_LENGTH(rt); + if (length < 2*8) + return -EBADMSG; + + *ret = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex; + return 0; +} + +int sd_ndisc_router_dnssl_get_domains(sd_ndisc_router *rt, char ***ret) { + _cleanup_strv_free_ char **l = NULL; + _cleanup_free_ char *e = NULL; + size_t n = 0, left; + uint8_t *ri, *p; + bool first = true; + int r; + unsigned k = 0; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_dnssl_info(rt, &ri); + if (r < 0) + return r; + + p = ri + 8; + left = NDISC_ROUTER_OPTION_LENGTH(rt) - 8; + + for (;;) { + if (left == 0) { + + if (n > 0) /* Not properly NUL terminated */ + return -EBADMSG; + + break; + } + + if (*p == 0) { + /* Found NUL termination */ + + if (n > 0) { + _cleanup_free_ char *normalized = NULL; + + e[n] = 0; + r = dns_name_normalize(e, 0, &normalized); + if (r < 0) + return r; + + /* Ignore the root domain name or "localhost" and friends */ + if (!is_localhost(normalized) && + !dns_name_is_root(normalized)) { + + if (strv_push(&l, normalized) < 0) + return -ENOMEM; + + normalized = NULL; + k++; + } + } + + n = 0; + first = true; + p++, left--; + continue; + } + + /* Check for compression (which is not allowed) */ + if (*p > 63) + return -EBADMSG; + + if (1U + *p + 1U > left) + return -EBADMSG; + + if (!GREEDY_REALLOC(e, n + !first + DNS_LABEL_ESCAPED_MAX + 1U)) + return -ENOMEM; + + if (first) + first = false; + else + e[n++] = '.'; + + r = dns_label_escape((char*) p+1, *p, e + n, DNS_LABEL_ESCAPED_MAX); + if (r < 0) + return r; + + n += r; + + left -= 1 + *p; + p += 1 + *p; + } + + if (strv_isempty(l)) { + *ret = NULL; + return 0; + } + + *ret = TAKE_PTR(l); + + return k; +} + +int sd_ndisc_router_dnssl_get_lifetime(sd_ndisc_router *rt, uint64_t *ret) { + uint8_t *ri; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_dnssl_info(rt, &ri); + if (r < 0) + return r; + + *ret = unaligned_be32_sec_to_usec(ri + 4, /* max_as_infinity = */ true); + return 0; +} + +int sd_ndisc_router_captive_portal_get_uri(sd_ndisc_router *rt, const char **ret, size_t *ret_size) { + int r; + const char *nd_opt_captive_portal; + size_t length; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + assert_return(ret_size, -EINVAL); + + r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_CAPTIVE_PORTAL); + if (r < 0) + return r; + if (r == 0) + return -EMEDIUMTYPE; + + r = sd_ndisc_router_option_get_raw(rt, (void *)&nd_opt_captive_portal, &length); + if (r < 0) + return r; + + /* The length field has units of 8 octets */ + assert(length % 8 == 0); + if (length == 0) + return -EBADMSG; + + /* Check that the message is not truncated by an embedded NUL. + * NUL padding to a multiple of 8 is expected. */ + size_t size = strnlen(nd_opt_captive_portal + 2, length - 2); + if (DIV_ROUND_UP(size + 2, 8) != length / 8) + return -EBADMSG; + + /* Let's not return an empty buffer */ + if (size == 0) { + *ret = NULL; + *ret_size = 0; + return 0; + } + + *ret = nd_opt_captive_portal + 2; + *ret_size = size; + + return 0; +} + +static int get_pref64_prefix_info(sd_ndisc_router *rt, struct nd_opt_prefix64_info **ret) { + struct nd_opt_prefix64_info *ri; + size_t length; + int r; + + assert(rt); + assert(ret); + + r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_PREF64); + if (r < 0) + return r; + if (r == 0) + return -EMEDIUMTYPE; + + length = NDISC_ROUTER_OPTION_LENGTH(rt); + if (length != sizeof(struct nd_opt_prefix64_info)) + return -EBADMSG; + + ri = (struct nd_opt_prefix64_info *) ((uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex); + if (!pref64_option_verify(ri, length)) + return -EBADMSG; + + *ret = ri; + return 0; +} + +int sd_ndisc_router_prefix64_get_prefix(sd_ndisc_router *rt, struct in6_addr *ret) { + struct nd_opt_prefix64_info *pi; + struct in6_addr a = {}; + unsigned prefixlen; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_pref64_prefix_info(rt, &pi); + if (r < 0) + return r; + + r = sd_ndisc_router_prefix64_get_prefixlen(rt, &prefixlen); + if (r < 0) + return r; + + memcpy(&a, pi->prefix, sizeof(pi->prefix)); + in6_addr_mask(&a, prefixlen); + /* extra safety check for refusing malformed prefix. */ + if (memcmp(&a, pi->prefix, sizeof(pi->prefix)) != 0) + return -EBADMSG; + + *ret = a; + return 0; +} + +int sd_ndisc_router_prefix64_get_prefixlen(sd_ndisc_router *rt, unsigned *ret) { + struct nd_opt_prefix64_info *pi; + uint16_t lifetime_prefix_len; + uint8_t prefix_len; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_pref64_prefix_info(rt, &pi); + if (r < 0) + return r; + + lifetime_prefix_len = be16toh(pi->lifetime_and_plc); + pref64_plc_to_prefix_length(lifetime_prefix_len, &prefix_len); + + *ret = prefix_len; + return 0; +} + +int sd_ndisc_router_prefix64_get_lifetime(sd_ndisc_router *rt, uint64_t *ret) { + struct nd_opt_prefix64_info *pi; + uint16_t lifetime_prefix_len; + int r; + + assert_return(rt, -EINVAL); + assert_return(ret, -EINVAL); + + r = get_pref64_prefix_info(rt, &pi); + if (r < 0) + return r; + + lifetime_prefix_len = be16toh(pi->lifetime_and_plc); + + *ret = (lifetime_prefix_len & PREF64_SCALED_LIFETIME_MASK) * USEC_PER_SEC; + return 0; +} diff --git a/src/libsystemd-network/ndisc-router.h b/src/libsystemd-network/ndisc-router.h new file mode 100644 index 0000000..0a55e1a --- /dev/null +++ b/src/libsystemd-network/ndisc-router.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include "sd-ndisc.h" + +#include "time-util.h" + +struct sd_ndisc_router { + unsigned n_ref; + + triple_timestamp timestamp; + struct in6_addr address; + + /* The raw packet size. The data is appended to the object, accessible via NDIS_ROUTER_RAW() */ + size_t raw_size; + + /* The current read index for the iterative option interface */ + size_t rindex; + + uint64_t flags; + unsigned preference; + uint64_t lifetime_usec; + + uint8_t hop_limit; + uint32_t mtu; + uint64_t icmp6_ratelimit_usec; +}; + +static inline void* NDISC_ROUTER_RAW(const sd_ndisc_router *rt) { + return (uint8_t*) rt + ALIGN(sizeof(sd_ndisc_router)); +} + +static inline void *NDISC_ROUTER_OPTION_DATA(const sd_ndisc_router *rt) { + return ((uint8_t*) NDISC_ROUTER_RAW(rt)) + rt->rindex; +} + +static inline uint8_t NDISC_ROUTER_OPTION_TYPE(const sd_ndisc_router *rt) { + return ((uint8_t*) NDISC_ROUTER_OPTION_DATA(rt))[0]; +} +static inline size_t NDISC_ROUTER_OPTION_LENGTH(const sd_ndisc_router *rt) { + return ((uint8_t*) NDISC_ROUTER_OPTION_DATA(rt))[1] * 8; +} + +sd_ndisc_router *ndisc_router_new(size_t raw_size); +int ndisc_router_parse(sd_ndisc *nd, sd_ndisc_router *rt); diff --git a/src/libsystemd-network/network-common.c b/src/libsystemd-network/network-common.c new file mode 100644 index 0000000..b639e9c --- /dev/null +++ b/src/libsystemd-network/network-common.c @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "env-util.h" +#include "format-util.h" +#include "network-common.h" +#include "socket-util.h" +#include "unaligned.h" + +int get_ifname(int ifindex, char **ifname) { + assert(ifname); + + /* This sets ifname only when it is not set yet. */ + + if (*ifname) + return 0; + + return format_ifname_alloc(ifindex, ifname); +} + +usec_t unaligned_be32_sec_to_usec(const void *p, bool max_as_infinity) { + uint32_t s = unaligned_read_be32(ASSERT_PTR(p)); + + if (s == UINT32_MAX && max_as_infinity) + return USEC_INFINITY; + + return s * USEC_PER_SEC; +} + +usec_t be32_sec_to_usec(be32_t t, bool max_as_infinity) { + uint32_t s = be32toh(t); + + if (s == UINT32_MAX && max_as_infinity) + return USEC_INFINITY; + + return s * USEC_PER_SEC; +} + +usec_t be32_msec_to_usec(be32_t t, bool max_as_infinity) { + uint32_t s = be32toh(t); + + if (s == UINT32_MAX && max_as_infinity) + return USEC_INFINITY; + + return s * USEC_PER_MSEC; +} + +usec_t be16_sec_to_usec(be16_t t, bool max_as_infinity) { + uint16_t s = be16toh(t); + + if (s == UINT16_MAX && max_as_infinity) + return USEC_INFINITY; + + return s * USEC_PER_SEC; +} + +be32_t usec_to_be32_sec(usec_t t) { + if (t == USEC_INFINITY) + /* Some settings, e.g. a lifetime of an address, UINT32_MAX is handled as infinity. so let's + * map USEC_INFINITY to UINT32_MAX. */ + return htobe32(UINT32_MAX); + + if (t >= (UINT32_MAX - 1) * USEC_PER_SEC) + /* Finite but too large. Let's use the largest (or off-by-one from the largest) finite value. */ + return htobe32(UINT32_MAX - 1); + + return htobe32((uint32_t) DIV_ROUND_UP(t, USEC_PER_SEC)); +} + +be32_t usec_to_be32_msec(usec_t t) { + if (t == USEC_INFINITY) + return htobe32(UINT32_MAX); + + if (t >= (UINT32_MAX - 1) * USEC_PER_MSEC) + return htobe32(UINT32_MAX - 1); + + return htobe32((uint32_t) DIV_ROUND_UP(t, USEC_PER_MSEC)); +} + +be16_t usec_to_be16_sec(usec_t t) { + if (t == USEC_INFINITY) + return htobe16(UINT16_MAX); + + if (t >= (UINT16_MAX - 1) * USEC_PER_SEC) + return htobe16(UINT16_MAX - 1); + + return htobe16((uint16_t) DIV_ROUND_UP(t, USEC_PER_SEC)); +} + +usec_t time_span_to_stamp(usec_t span, usec_t base) { + /* Typically, 0 lifetime (timespan) indicates the corresponding configuration (address or so) must be + * dropped. So, when the timespan is zero, here we return 0 rather than 'base'. This makes the caller + * easily understand that the configuration needs to be dropped immediately. */ + if (span == 0) + return 0; + + return usec_add(base, span); +} + +bool network_test_mode_enabled(void) { + static int test_mode = -1; + int r; + + if (test_mode < 0) { + r = getenv_bool("SYSTEMD_NETWORK_TEST_MODE"); + if (r < 0) { + if (r != -ENXIO) + log_debug_errno(r, "Failed to parse $SYSTEMD_NETWORK_TEST_MODE environment variable, ignoring: %m"); + + test_mode = false; + } else + test_mode = r; + } + + return test_mode; +} + +triple_timestamp* triple_timestamp_from_cmsg(triple_timestamp *t, struct msghdr *mh) { + assert(t); + assert(mh); + + struct timeval *tv = CMSG_FIND_AND_COPY_DATA(mh, SOL_SOCKET, SCM_TIMESTAMP, struct timeval); + if (tv) + return triple_timestamp_from_realtime(t, timeval_load(tv)); + + return triple_timestamp_now(t); +} diff --git a/src/libsystemd-network/network-common.h b/src/libsystemd-network/network-common.h new file mode 100644 index 0000000..1750f18 --- /dev/null +++ b/src/libsystemd-network/network-common.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <sys/socket.h> + +#include "log-link.h" +#include "sparse-endian.h" +#include "time-util.h" + +#define log_interface_prefix_full_errno_zerook(prefix, type, val, error, fmt, ...) \ + ({ \ + int _e = (error); \ + if (DEBUG_LOGGING) { \ + const char *_n = NULL; \ + type *_v = (val); \ + \ + if (_v) \ + (void) type##_get_ifname(_v, &_n); \ + log_interface_full_errno_zerook( \ + _n, LOG_DEBUG, _e, prefix fmt, \ + ##__VA_ARGS__); \ + } \ + -ERRNO_VALUE(_e); \ + }) + +#define log_interface_prefix_full_errno(prefix, type, val, error, fmt, ...) \ + ({ \ + int _error = (error); \ + ASSERT_NON_ZERO(_error); \ + log_interface_prefix_full_errno_zerook( \ + prefix, type, val, _error, fmt, ##__VA_ARGS__); \ + }) + +int get_ifname(int ifindex, char **ifname); + +usec_t unaligned_be32_sec_to_usec(const void *p, bool max_as_infinity); +usec_t be32_sec_to_usec(be32_t t, bool max_as_infinity); +usec_t be32_msec_to_usec(be32_t t, bool max_as_infinity); +usec_t be16_sec_to_usec(be16_t t, bool max_as_infinity); +be32_t usec_to_be32_sec(usec_t t); +be32_t usec_to_be32_msec(usec_t t); +be16_t usec_to_be16_sec(usec_t t); +usec_t time_span_to_stamp(usec_t span, usec_t base); + +bool network_test_mode_enabled(void); + +triple_timestamp* triple_timestamp_from_cmsg(triple_timestamp *t, struct msghdr *mh); +#define TRIPLE_TIMESTAMP_FROM_CMSG(mh) \ + triple_timestamp_from_cmsg(&(triple_timestamp) {}, mh) diff --git a/src/libsystemd-network/network-internal.c b/src/libsystemd-network/network-internal.c new file mode 100644 index 0000000..c8aa021 --- /dev/null +++ b/src/libsystemd-network/network-internal.c @@ -0,0 +1,239 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <linux/if.h> +#include <netinet/ether.h> + +#include "sd-ndisc.h" + +#include "alloc-util.h" +#include "dhcp-lease-internal.h" +#include "extract-word.h" +#include "hexdecoct.h" +#include "in-addr-util.h" +#include "log.h" +#include "network-internal.h" +#include "parse-util.h" + +size_t serialize_in_addrs(FILE *f, + const struct in_addr *addresses, + size_t size, + bool *with_leading_space, + bool (*predicate)(const struct in_addr *addr)) { + assert(f); + assert(addresses); + + size_t count = 0; + bool _space = false; + if (!with_leading_space) + with_leading_space = &_space; + + for (size_t i = 0; i < size; i++) { + if (predicate && !predicate(&addresses[i])) + continue; + + if (*with_leading_space) + fputc(' ', f); + fputs(IN4_ADDR_TO_STRING(&addresses[i]), f); + count++; + *with_leading_space = true; + } + + return count; +} + +int deserialize_in_addrs(struct in_addr **ret, const char *string) { + _cleanup_free_ struct in_addr *addresses = NULL; + int size = 0; + + assert(ret); + assert(string); + + for (;;) { + _cleanup_free_ char *word = NULL; + struct in_addr *new_addresses; + int r; + + r = extract_first_word(&string, &word, NULL, 0); + if (r < 0) + return r; + if (r == 0) + break; + + new_addresses = reallocarray(addresses, size + 1, sizeof(struct in_addr)); + if (!new_addresses) + return -ENOMEM; + else + addresses = new_addresses; + + r = inet_pton(AF_INET, word, &(addresses[size])); + if (r <= 0) + continue; + + size++; + } + + *ret = size > 0 ? TAKE_PTR(addresses) : NULL; + + return size; +} + +void serialize_in6_addrs(FILE *f, const struct in6_addr *addresses, size_t size, bool *with_leading_space) { + assert(f); + assert(addresses); + assert(size); + + bool _space = false; + if (!with_leading_space) + with_leading_space = &_space; + + for (size_t i = 0; i < size; i++) { + if (*with_leading_space) + fputc(' ', f); + fputs(IN6_ADDR_TO_STRING(&addresses[i]), f); + *with_leading_space = true; + } +} + +int deserialize_in6_addrs(struct in6_addr **ret, const char *string) { + _cleanup_free_ struct in6_addr *addresses = NULL; + int size = 0; + + assert(ret); + assert(string); + + for (;;) { + _cleanup_free_ char *word = NULL; + struct in6_addr *new_addresses; + int r; + + r = extract_first_word(&string, &word, NULL, 0); + if (r < 0) + return r; + if (r == 0) + break; + + new_addresses = reallocarray(addresses, size + 1, sizeof(struct in6_addr)); + if (!new_addresses) + return -ENOMEM; + else + addresses = new_addresses; + + r = inet_pton(AF_INET6, word, &(addresses[size])); + if (r <= 0) + continue; + + size++; + } + + *ret = TAKE_PTR(addresses); + + return size; +} + +void serialize_dhcp_routes(FILE *f, const char *key, sd_dhcp_route **routes, size_t size) { + assert(f); + assert(key); + assert(routes); + assert(size); + + fprintf(f, "%s=", key); + + for (size_t i = 0; i < size; i++) { + struct in_addr dest, gw; + uint8_t length; + + assert_se(sd_dhcp_route_get_destination(routes[i], &dest) >= 0); + assert_se(sd_dhcp_route_get_gateway(routes[i], &gw) >= 0); + assert_se(sd_dhcp_route_get_destination_prefix_length(routes[i], &length) >= 0); + + fprintf(f, "%s,%s%s", + IN4_ADDR_PREFIX_TO_STRING(&dest, length), + IN4_ADDR_TO_STRING(&gw), + i < size - 1 ? " ": ""); + } + + fputs("\n", f); +} + +int deserialize_dhcp_routes(struct sd_dhcp_route **ret, size_t *ret_size, const char *string) { + _cleanup_free_ struct sd_dhcp_route *routes = NULL; + size_t size = 0; + + assert(ret); + assert(ret_size); + assert(string); + + /* WORD FORMAT: dst_ip/dst_prefixlen,gw_ip */ + for (;;) { + _cleanup_free_ char *word = NULL; + char *tok, *tok_end; + unsigned n; + int r; + + r = extract_first_word(&string, &word, NULL, 0); + if (r < 0) + return r; + if (r == 0) + break; + + if (!GREEDY_REALLOC(routes, size + 1)) + return -ENOMEM; + + tok = word; + + /* get the subnet */ + tok_end = strchr(tok, '/'); + if (!tok_end) + continue; + *tok_end = '\0'; + + r = inet_aton(tok, &routes[size].dst_addr); + if (r == 0) + continue; + + tok = tok_end + 1; + + /* get the prefixlen */ + tok_end = strchr(tok, ','); + if (!tok_end) + continue; + + *tok_end = '\0'; + + r = safe_atou(tok, &n); + if (r < 0 || n > 32) + continue; + + routes[size].dst_prefixlen = (uint8_t) n; + tok = tok_end + 1; + + /* get the gateway */ + r = inet_aton(tok, &routes[size].gw_addr); + if (r == 0) + continue; + + size++; + } + + *ret_size = size; + *ret = TAKE_PTR(routes); + + return 0; +} + +int serialize_dhcp_option(FILE *f, const char *key, const void *data, size_t size) { + _cleanup_free_ char *hex_buf = NULL; + + assert(f); + assert(key); + assert(data); + + hex_buf = hexmem(data, size); + if (!hex_buf) + return -ENOMEM; + + fprintf(f, "%s=%s\n", key, hex_buf); + + return 0; +} diff --git a/src/libsystemd-network/network-internal.h b/src/libsystemd-network/network-internal.h new file mode 100644 index 0000000..5aa225e --- /dev/null +++ b/src/libsystemd-network/network-internal.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> +#include <stdio.h> + +#include "sd-dhcp-lease.h" + +size_t serialize_in_addrs(FILE *f, + const struct in_addr *addresses, + size_t size, + bool *with_leading_space, + bool (*predicate)(const struct in_addr *addr)); +int deserialize_in_addrs(struct in_addr **addresses, const char *string); +void serialize_in6_addrs(FILE *f, const struct in6_addr *addresses, + size_t size, + bool *with_leading_space); +int deserialize_in6_addrs(struct in6_addr **addresses, const char *string); + +/* don't include "dhcp-lease-internal.h" as it causes conflicts between netinet/ip.h and linux/ip.h */ +struct sd_dhcp_route; +struct sd_dhcp_lease; + +void serialize_dhcp_routes(FILE *f, const char *key, struct sd_dhcp_route **routes, size_t size); +int deserialize_dhcp_routes(struct sd_dhcp_route **ret, size_t *ret_size, const char *string); + +/* It is not necessary to add deserialize_dhcp_option(). Use unhexmem() instead. */ +int serialize_dhcp_option(FILE *f, const char *key, const void *data, size_t size); + +int dhcp_lease_save(sd_dhcp_lease *lease, const char *lease_file); +int dhcp_lease_load(sd_dhcp_lease **ret, const char *lease_file); diff --git a/src/libsystemd-network/radv-internal.h b/src/libsystemd-network/radv-internal.h new file mode 100644 index 0000000..d6cec90 --- /dev/null +++ b/src/libsystemd-network/radv-internal.h @@ -0,0 +1,222 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/*** + Copyright © 2017 Intel Corporation. All rights reserved. +***/ + +#include <netinet/icmp6.h> + +#include "sd-radv.h" + +#include "list.h" +#include "ndisc-protocol.h" +#include "network-common.h" +#include "sparse-endian.h" +#include "time-util.h" + +/* RFC 4861 section 6.2.1. + * MaxRtrAdvInterval + * The maximum time allowed between sending unsolicited multicast Router Advertisements from the + * interface, in seconds. MUST be no less than 4 seconds and no greater than 1800 seconds. + * Default: 600 seconds */ +#define RADV_MIN_MAX_TIMEOUT_USEC (4 * USEC_PER_SEC) +#define RADV_MAX_MAX_TIMEOUT_USEC (1800 * USEC_PER_SEC) +#define RADV_DEFAULT_MAX_TIMEOUT_USEC (600 * USEC_PER_SEC) +/* RFC 4861 section 6.2.1. + * MinRtrAdvInterval + * The minimum time allowed between sending unsolicited multicast Router Advertisements from the + * interface, in seconds. MUST be no less than 3 seconds and no greater than .75 * MaxRtrAdvInterval. + * Default: 0.33 * MaxRtrAdvInterval If MaxRtrAdvInterval >= 9 seconds; otherwise, the Default is + * MaxRtrAdvInterval (Note, this should be a typo. We use 0.75 * MaxRtrAdvInterval). */ +#define RADV_MIN_MIN_TIMEOUT_USEC (3 * USEC_PER_SEC) +/* RFC 4861 section 6.2.4. + * AdvDefaultLifetime + * The value to be placed in the Router Lifetime field of Router Advertisements sent from the interface, + * in seconds. MUST be either zero or between MaxRtrAdvInterval and 9000 seconds. A value of zero + * indicates that the router is not to be used as a default router. These limits may be overridden by + * specific documents that describe how IPv6 operates over different link layers. For instance, in a + * point-to-point link the peers may have enough information about the number and status of devices at + * the other end so that advertisements are needed less frequently. + * Default: 3 * MaxRtrAdvInterval */ +#define RADV_MIN_ROUTER_LIFETIME_USEC RADV_MIN_MAX_TIMEOUT_USEC +#define RADV_MAX_ROUTER_LIFETIME_USEC (9000 * USEC_PER_SEC) +#define RADV_DEFAULT_ROUTER_LIFETIME_USEC (3 * RADV_DEFAULT_MAX_TIMEOUT_USEC) +/* RFC 4861 section 4.2. + * Retrans Timer + * 32-bit unsigned integer. The time, in milliseconds. */ +#define RADV_MAX_RETRANSMIT_USEC (UINT32_MAX * USEC_PER_MSEC) +/* draft-ietf-6man-slaac-renum-02 section 4.1.1. + * AdvPreferredLifetime: max(AdvDefaultLifetime, 3 * MaxRtrAdvInterval) + * AdvValidLifetime: 2 * AdvPreferredLifetime */ +#define RADV_DEFAULT_PREFERRED_LIFETIME_USEC CONST_MAX(RADV_DEFAULT_ROUTER_LIFETIME_USEC, 3 * RADV_DEFAULT_MAX_TIMEOUT_USEC) +#define RADV_DEFAULT_VALID_LIFETIME_USEC (2 * RADV_DEFAULT_PREFERRED_LIFETIME_USEC) +/* RFC 4861 section 10. + * MAX_INITIAL_RTR_ADVERT_INTERVAL 16 seconds + * MAX_INITIAL_RTR_ADVERTISEMENTS 3 transmissions + * MAX_FINAL_RTR_ADVERTISEMENTS 3 transmissions + * MIN_DELAY_BETWEEN_RAS 3 seconds + * MAX_RA_DELAY_TIME .5 seconds */ +#define RADV_MAX_INITIAL_RTR_ADVERT_INTERVAL_USEC (16 * USEC_PER_SEC) +#define RADV_MAX_INITIAL_RTR_ADVERTISEMENTS 3 +#define RADV_MAX_FINAL_RTR_ADVERTISEMENTS 3 +#define RADV_MIN_DELAY_BETWEEN_RAS 3 +#define RADV_MAX_RA_DELAY_TIME_USEC (500 * USEC_PER_MSEC) +/* From RFC 8781 section 4.1 + * By default, the value of the Scaled Lifetime field SHOULD be set to the lesser of 3 x MaxRtrAdvInterval */ +#define RADV_PREF64_DEFAULT_LIFETIME_USEC (3 * RADV_DEFAULT_MAX_TIMEOUT_USEC) + +#define RADV_RDNSS_MAX_LIFETIME_USEC (UINT32_MAX * USEC_PER_SEC) +#define RADV_DNSSL_MAX_LIFETIME_USEC (UINT32_MAX * USEC_PER_SEC) +/* rfc6275 7.4 Neighbor Discovery Home Agent Lifetime. + * The default value is the same as the Router Lifetime. + * The maximum value corresponds to 18.2 hours. 0 MUST NOT be used. */ +#define RADV_HOME_AGENT_MAX_LIFETIME_USEC (UINT16_MAX * USEC_PER_SEC) + +#define RADV_OPT_ROUTE_INFORMATION 24 +#define RADV_OPT_RDNSS 25 +#define RADV_OPT_DNSSL 31 +/* Pref64 option type (RFC8781, section 4) */ +#define RADV_OPT_PREF64 38 + +enum RAdvState { + RADV_STATE_IDLE = 0, + RADV_STATE_ADVERTISING = 1, +}; +typedef enum RAdvState RAdvState; + +struct sd_radv_opt_dns { + uint8_t type; + uint8_t length; + uint16_t reserved; + be32_t lifetime; +} _packed_; + +struct sd_radv { + unsigned n_ref; + RAdvState state; + + int ifindex; + char *ifname; + + sd_event *event; + int event_priority; + + struct ether_addr mac_addr; + uint8_t hop_limit; + uint8_t flags; + uint32_t mtu; + usec_t retransmit_usec; + usec_t lifetime_usec; /* timespan */ + + int fd; + unsigned ra_sent; + sd_event_source *recv_event_source; + sd_event_source *timeout_event_source; + + unsigned n_prefixes; + LIST_HEAD(sd_radv_prefix, prefixes); + + unsigned n_route_prefixes; + LIST_HEAD(sd_radv_route_prefix, route_prefixes); + + unsigned n_pref64_prefixes; + LIST_HEAD(sd_radv_pref64_prefix, pref64_prefixes); + + size_t n_rdnss; + struct sd_radv_opt_dns *rdnss; + struct sd_radv_opt_dns *dnssl; + + /* Mobile IPv6 extension: Home Agent Info. */ + struct nd_opt_home_agent_info home_agent; +}; + +#define radv_prefix_opt__contents { \ + uint8_t type; \ + uint8_t length; \ + uint8_t prefixlen; \ + uint8_t flags; \ + be32_t lifetime_valid; \ + be32_t lifetime_preferred; \ + uint32_t reserved; \ + struct in6_addr in6_addr; \ +} + +struct radv_prefix_opt radv_prefix_opt__contents; + +/* We need the opt substructure to be packed, because we use it in send(). But + * if we use _packed_, this means that the structure cannot be used directly in + * normal code in general, because the fields might not be properly aligned. + * But in this particular case, the structure is defined in a way that gives + * proper alignment, even without the explicit _packed_ attribute. To appease + * the compiler we use the "unpacked" structure, but we also verify that + * structure contains no holes, so offsets are the same when _packed_ is used. + */ +struct radv_prefix_opt__packed radv_prefix_opt__contents _packed_; +assert_cc(sizeof(struct radv_prefix_opt) == sizeof(struct radv_prefix_opt__packed)); + +struct sd_radv_prefix { + unsigned n_ref; + + struct radv_prefix_opt opt; + + LIST_FIELDS(struct sd_radv_prefix, prefix); + + /* These are timespans, NOT points in time. */ + usec_t lifetime_valid_usec; + usec_t lifetime_preferred_usec; + /* These are points in time specified with clock_boottime_or_monotonic(), NOT timespans. */ + usec_t valid_until; + usec_t preferred_until; +}; + +#define radv_route_prefix_opt__contents { \ + uint8_t type; \ + uint8_t length; \ + uint8_t prefixlen; \ + uint8_t flags_reserved; \ + be32_t lifetime; \ + struct in6_addr in6_addr; \ +} + +struct radv_route_prefix_opt radv_route_prefix_opt__contents; + +struct radv_route_prefix_opt__packed radv_route_prefix_opt__contents _packed_; +assert_cc(sizeof(struct radv_route_prefix_opt) == sizeof(struct radv_route_prefix_opt__packed)); + +struct sd_radv_route_prefix { + unsigned n_ref; + + struct radv_route_prefix_opt opt; + + LIST_FIELDS(struct sd_radv_route_prefix, prefix); + + /* This is a timespan, NOT a point in time. */ + usec_t lifetime_usec; + /* This is a point in time specified with clock_boottime_or_monotonic(), NOT a timespan. */ + usec_t valid_until; +}; + +struct sd_radv_pref64_prefix { + unsigned n_ref; + + struct nd_opt_prefix64_info opt; + + struct in6_addr in6_addr; + uint8_t prefixlen; + + usec_t lifetime_usec; + + LIST_FIELDS(struct sd_radv_pref64_prefix, prefix); +}; + +#define log_radv_errno(radv, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "RADV: ", \ + sd_radv, radv, \ + error, fmt, ##__VA_ARGS__) +#define log_radv(radv, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "RADV: ", \ + sd_radv, radv, \ + 0, fmt, ##__VA_ARGS__) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c new file mode 100644 index 0000000..24bcd74 --- /dev/null +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -0,0 +1,2568 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <net/ethernet.h> +#include <net/if_arp.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <linux/if_infiniband.h> + +#include "sd-dhcp-client.h" + +#include "alloc-util.h" +#include "device-util.h" +#include "dhcp-client-internal.h" +#include "dhcp-identifier.h" +#include "dhcp-lease-internal.h" +#include "dhcp-network.h" +#include "dhcp-option.h" +#include "dhcp-packet.h" +#include "dns-domain.h" +#include "ether-addr-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "hostname-util.h" +#include "iovec-util.h" +#include "memory-util.h" +#include "network-common.h" +#include "random-util.h" +#include "set.h" +#include "sort-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "time-util.h" +#include "utf8.h" +#include "web-util.h" + +#define MAX_CLIENT_ID_LEN (sizeof(uint32_t) + MAX_DUID_LEN) /* Arbitrary limit */ +#define MAX_MAC_ADDR_LEN CONST_MAX(INFINIBAND_ALEN, ETH_ALEN) + +#define RESTART_AFTER_NAK_MIN_USEC (1 * USEC_PER_SEC) +#define RESTART_AFTER_NAK_MAX_USEC (30 * USEC_PER_MINUTE) + +#define TRANSIENT_FAILURE_ATTEMPTS 3 /* Arbitrary limit: how many attempts are considered enough to report + * transient failure. */ + +typedef struct sd_dhcp_client_id { + uint8_t type; + union { + struct { + /* 0: Generic (non-LL) (RFC 2132) */ + uint8_t data[MAX_CLIENT_ID_LEN]; + } _packed_ gen; + struct { + /* 1: Ethernet Link-Layer (RFC 2132) */ + uint8_t haddr[ETH_ALEN]; + } _packed_ eth; + struct { + /* 2 - 254: ARP/Link-Layer (RFC 2132) */ + uint8_t haddr[0]; + } _packed_ ll; + struct { + /* 255: Node-specific (RFC 4361) */ + be32_t iaid; + struct duid duid; + } _packed_ ns; + struct { + uint8_t data[MAX_CLIENT_ID_LEN]; + } _packed_ raw; + }; +} _packed_ sd_dhcp_client_id; + +struct sd_dhcp_client { + unsigned n_ref; + + DHCPState state; + sd_event *event; + int event_priority; + sd_event_source *timeout_resend; + + int ifindex; + char *ifname; + + sd_device *dev; + + int fd; + uint16_t port; + union sockaddr_union link; + sd_event_source *receive_message; + bool request_broadcast; + Set *req_opts; + bool anonymize; + bool rapid_commit; + be32_t last_addr; + struct hw_addr_data hw_addr; + struct hw_addr_data bcast_addr; + uint16_t arp_type; + sd_dhcp_client_id client_id; + size_t client_id_len; + char *hostname; + char *vendor_class_identifier; + char *mudurl; + char **user_class; + uint32_t mtu; + usec_t fallback_lease_lifetime; + uint32_t xid; + usec_t start_time; + usec_t t1_time; + usec_t t2_time; + usec_t expire_time; + uint64_t discover_attempt; + uint64_t request_attempt; + uint64_t max_discover_attempts; + uint64_t max_request_attempts; + OrderedHashmap *extra_options; + OrderedHashmap *vendor_options; + sd_event_source *timeout_t1; + sd_event_source *timeout_t2; + sd_event_source *timeout_expire; + sd_event_source *timeout_ipv6_only_mode; + sd_dhcp_client_callback_t callback; + void *userdata; + sd_dhcp_client_callback_t state_callback; + void *state_userdata; + sd_dhcp_lease *lease; + usec_t start_delay; + int ip_service_type; + int socket_priority; + bool socket_priority_set; + bool ipv6_acquired; +}; + +static const uint8_t default_req_opts[] = { + SD_DHCP_OPTION_SUBNET_MASK, + SD_DHCP_OPTION_ROUTER, + SD_DHCP_OPTION_HOST_NAME, + SD_DHCP_OPTION_DOMAIN_NAME, + SD_DHCP_OPTION_DOMAIN_NAME_SERVER, +}; + +/* RFC7844 section 3: + MAY contain the Parameter Request List option. + RFC7844 section 3.6: + The client intending to protect its privacy SHOULD only request a + minimal number of options in the PRL and SHOULD also randomly shuffle + the ordering of option codes in the PRL. If this random ordering + cannot be implemented, the client MAY order the option codes in the + PRL by option code number (lowest to highest). +*/ +/* NOTE: using PRL options that Windows 10 RFC7844 implementation uses */ +static const uint8_t default_req_opts_anonymize[] = { + SD_DHCP_OPTION_SUBNET_MASK, /* 1 */ + SD_DHCP_OPTION_ROUTER, /* 3 */ + SD_DHCP_OPTION_DOMAIN_NAME_SERVER, /* 6 */ + SD_DHCP_OPTION_DOMAIN_NAME, /* 15 */ + SD_DHCP_OPTION_ROUTER_DISCOVERY, /* 31 */ + SD_DHCP_OPTION_STATIC_ROUTE, /* 33 */ + SD_DHCP_OPTION_VENDOR_SPECIFIC, /* 43 */ + SD_DHCP_OPTION_NETBIOS_NAME_SERVER, /* 44 */ + SD_DHCP_OPTION_NETBIOS_NODE_TYPE, /* 46 */ + SD_DHCP_OPTION_NETBIOS_SCOPE, /* 47 */ + SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE, /* 121 */ + SD_DHCP_OPTION_PRIVATE_CLASSLESS_STATIC_ROUTE, /* 249 */ + SD_DHCP_OPTION_PRIVATE_PROXY_AUTODISCOVERY, /* 252 */ +}; + +static int client_receive_message_raw( + sd_event_source *s, + int fd, + uint32_t revents, + void *userdata); +static int client_receive_message_udp( + sd_event_source *s, + int fd, + uint32_t revents, + void *userdata); +static void client_stop(sd_dhcp_client *client, int error); +static int client_restart(sd_dhcp_client *client); + +int sd_dhcp_client_id_to_string(const void *data, size_t len, char **ret) { + const sd_dhcp_client_id *client_id = data; + _cleanup_free_ char *t = NULL; + int r = 0; + + assert_return(data, -EINVAL); + assert_return(len >= 1, -EINVAL); + assert_return(ret, -EINVAL); + + len -= 1; + if (len > MAX_CLIENT_ID_LEN) + return -EINVAL; + + switch (client_id->type) { + case 0: + if (utf8_is_printable((char *) client_id->gen.data, len)) + r = asprintf(&t, "%.*s", (int) len, client_id->gen.data); + else + r = asprintf(&t, "DATA"); + break; + case 1: + if (len == sizeof_field(sd_dhcp_client_id, eth)) + r = asprintf(&t, "%02x:%02x:%02x:%02x:%02x:%02x", + client_id->eth.haddr[0], + client_id->eth.haddr[1], + client_id->eth.haddr[2], + client_id->eth.haddr[3], + client_id->eth.haddr[4], + client_id->eth.haddr[5]); + else + r = asprintf(&t, "ETHER"); + break; + case 2 ... 254: + r = asprintf(&t, "ARP/LL"); + break; + case 255: + if (len < sizeof(uint32_t)) + r = asprintf(&t, "IAID/DUID"); + else { + uint32_t iaid = be32toh(client_id->ns.iaid); + /* TODO: check and stringify DUID */ + r = asprintf(&t, "IAID:0x%x/DUID", iaid); + } + break; + } + if (r < 0) + return -ENOMEM; + + *ret = TAKE_PTR(t); + return 0; +} + +int dhcp_client_set_state_callback( + sd_dhcp_client *client, + sd_dhcp_client_callback_t cb, + void *userdata) { + + assert_return(client, -EINVAL); + + client->state_callback = cb; + client->state_userdata = userdata; + + return 0; +} + +int sd_dhcp_client_set_callback( + sd_dhcp_client *client, + sd_dhcp_client_callback_t cb, + void *userdata) { + + assert_return(client, -EINVAL); + + client->callback = cb; + client->userdata = userdata; + + return 0; +} + +int sd_dhcp_client_set_request_broadcast(sd_dhcp_client *client, int broadcast) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + client->request_broadcast = broadcast; + + return 0; +} + +int sd_dhcp_client_set_request_option(sd_dhcp_client *client, uint8_t option) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + switch (option) { + + case SD_DHCP_OPTION_PAD: + case SD_DHCP_OPTION_OVERLOAD: + case SD_DHCP_OPTION_MESSAGE_TYPE: + case SD_DHCP_OPTION_PARAMETER_REQUEST_LIST: + case SD_DHCP_OPTION_END: + return -EINVAL; + + default: + break; + } + + return set_ensure_put(&client->req_opts, NULL, UINT8_TO_PTR(option)); +} + +static int client_request_contains(sd_dhcp_client *client, uint8_t option) { + assert(client); + + return set_contains(client->req_opts, UINT8_TO_PTR(option)); +} + +int sd_dhcp_client_set_request_address( + sd_dhcp_client *client, + const struct in_addr *last_addr) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + if (last_addr) + client->last_addr = last_addr->s_addr; + else + client->last_addr = INADDR_ANY; + + return 0; +} + +int sd_dhcp_client_set_ifindex(sd_dhcp_client *client, int ifindex) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(ifindex > 0, -EINVAL); + + client->ifindex = ifindex; + return 0; +} + +int sd_dhcp_client_set_ifname(sd_dhcp_client *client, const char *ifname) { + assert_return(client, -EINVAL); + assert_return(ifname, -EINVAL); + + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) + return -EINVAL; + + return free_and_strdup(&client->ifname, ifname); +} + +int sd_dhcp_client_get_ifname(sd_dhcp_client *client, const char **ret) { + int r; + + assert_return(client, -EINVAL); + + r = get_ifname(client->ifindex, &client->ifname); + if (r < 0) + return r; + + if (ret) + *ret = client->ifname; + + return 0; +} + +int sd_dhcp_client_set_mac( + sd_dhcp_client *client, + const uint8_t *hw_addr, + const uint8_t *bcast_addr, + size_t addr_len, + uint16_t arp_type) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(IN_SET(arp_type, ARPHRD_ETHER, ARPHRD_INFINIBAND), -EINVAL); + assert_return(hw_addr, -EINVAL); + assert_return(addr_len == (arp_type == ARPHRD_ETHER ? ETH_ALEN : INFINIBAND_ALEN), -EINVAL); + + client->arp_type = arp_type; + hw_addr_set(&client->hw_addr, hw_addr, addr_len); + hw_addr_set(&client->bcast_addr, bcast_addr, bcast_addr ? addr_len : 0); + + return 0; +} + +int sd_dhcp_client_get_client_id( + sd_dhcp_client *client, + uint8_t *ret_type, + const uint8_t **ret_data, + size_t *ret_data_len) { + + assert_return(client, -EINVAL); + + if (client->client_id_len > 0) { + if (client->client_id_len <= offsetof(sd_dhcp_client_id, raw.data)) + return -EINVAL; + + if (ret_type) + *ret_type = client->client_id.type; + if (ret_data) + *ret_data = client->client_id.raw.data; + if (ret_data_len) + *ret_data_len = client->client_id_len - offsetof(sd_dhcp_client_id, raw.data); + return 1; + } + + if (ret_type) + *ret_type = 0; + if (ret_data) + *ret_data = NULL; + if (ret_data_len) + *ret_data_len = 0; + + return 0; +} + +int sd_dhcp_client_set_client_id( + sd_dhcp_client *client, + uint8_t type, + const uint8_t *data, + size_t data_len) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(data, -EINVAL); + assert_return(data_len > 0 && data_len <= MAX_CLIENT_ID_LEN, -EINVAL); + + /* For hardware types, log debug message about unexpected data length. + * + * Note that infiniband's INFINIBAND_ALEN is 20 bytes long, but only + * the last 8 bytes of the address are stable and suitable to put into + * the client-id. The caller is advised to account for that. */ + if ((type == ARPHRD_ETHER && data_len != ETH_ALEN) || + (type == ARPHRD_INFINIBAND && data_len != 8)) + log_dhcp_client(client, + "Changing client ID to hardware type %u with unexpected address length %zu", + type, data_len); + + client->client_id.type = type; + memcpy(&client->client_id.raw.data, data, data_len); + client->client_id_len = data_len + sizeof (client->client_id.type); + + return 0; +} + +/** + * Sets IAID and DUID. If duid is non-null, the DUID is set to duid_type + duid + * without further modification. Otherwise, if duid_type is supported, DUID + * is set based on that type. Otherwise, an error is returned. + */ +static int dhcp_client_set_iaid( + sd_dhcp_client *client, + bool iaid_set, + uint32_t iaid) { + + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + zero(client->client_id); + client->client_id.type = 255; + + if (iaid_set) + client->client_id.ns.iaid = htobe32(iaid); + else { + r = dhcp_identifier_set_iaid(client->dev, &client->hw_addr, + /* legacy_unstable_byteorder = */ true, + &client->client_id.ns.iaid); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to set IAID: %m"); + } + + return 0; +} + +int sd_dhcp_client_set_iaid_duid_llt( + sd_dhcp_client *client, + bool iaid_set, + uint32_t iaid, + usec_t llt_time) { + + size_t len; + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + r = dhcp_client_set_iaid(client, iaid_set, iaid); + if (r < 0) + return r; + + r = dhcp_identifier_set_duid_llt(&client->hw_addr, client->arp_type, llt_time, &client->client_id.ns.duid, &len); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to set DUID-LLT: %m"); + + client->client_id_len = sizeof(client->client_id.type) + sizeof(client->client_id.ns.iaid) + len; + + return 0; +} + +int sd_dhcp_client_set_iaid_duid_ll( + sd_dhcp_client *client, + bool iaid_set, + uint32_t iaid) { + + size_t len; + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + r = dhcp_client_set_iaid(client, iaid_set, iaid); + if (r < 0) + return r; + + r = dhcp_identifier_set_duid_ll(&client->hw_addr, client->arp_type, &client->client_id.ns.duid, &len); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to set DUID-LL: %m"); + + client->client_id_len = sizeof(client->client_id.type) + sizeof(client->client_id.ns.iaid) + len; + + return 0; +} + +int sd_dhcp_client_set_iaid_duid_en( + sd_dhcp_client *client, + bool iaid_set, + uint32_t iaid) { + + size_t len; + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + r = dhcp_client_set_iaid(client, iaid_set, iaid); + if (r < 0) + return r; + + r = dhcp_identifier_set_duid_en(&client->client_id.ns.duid, &len); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to set DUID-EN: %m"); + + client->client_id_len = sizeof(client->client_id.type) + sizeof(client->client_id.ns.iaid) + len; + + return 0; +} + +int sd_dhcp_client_set_iaid_duid_uuid( + sd_dhcp_client *client, + bool iaid_set, + uint32_t iaid) { + + size_t len; + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + r = dhcp_client_set_iaid(client, iaid_set, iaid); + if (r < 0) + return r; + + r = dhcp_identifier_set_duid_uuid(&client->client_id.ns.duid, &len); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to set DUID-UUID: %m"); + + client->client_id_len = sizeof(client->client_id.type) + sizeof(client->client_id.ns.iaid) + len; + + return 0; +} + +int sd_dhcp_client_set_iaid_duid_raw( + sd_dhcp_client *client, + bool iaid_set, + uint32_t iaid, + uint16_t duid_type, + const uint8_t *duid, + size_t duid_len) { + + size_t len; + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(duid || duid_len == 0, -EINVAL); + + r = dhcp_client_set_iaid(client, iaid_set, iaid); + if (r < 0) + return r; + + r = dhcp_identifier_set_duid_raw(duid_type, duid, duid_len, &client->client_id.ns.duid, &len); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to set DUID: %m"); + + client->client_id_len = sizeof(client->client_id.type) + sizeof(client->client_id.ns.iaid) + len; + + return 0; +} + +int sd_dhcp_client_set_rapid_commit(sd_dhcp_client *client, bool rapid_commit) { + assert_return(client, -EINVAL); + + client->rapid_commit = !client->anonymize && rapid_commit; + return 0; +} + +int sd_dhcp_client_set_hostname( + sd_dhcp_client *client, + const char *hostname) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + /* Make sure hostnames qualify as DNS and as Linux hostnames */ + if (hostname && + !(hostname_is_valid(hostname, 0) && dns_name_is_valid(hostname) > 0)) + return -EINVAL; + + return free_and_strdup(&client->hostname, hostname); +} + +int sd_dhcp_client_set_vendor_class_identifier( + sd_dhcp_client *client, + const char *vci) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + return free_and_strdup(&client->vendor_class_identifier, vci); +} + +int sd_dhcp_client_set_mud_url( + sd_dhcp_client *client, + const char *mudurl) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(mudurl, -EINVAL); + assert_return(strlen(mudurl) <= 255, -EINVAL); + assert_return(http_url_is_valid(mudurl), -EINVAL); + + return free_and_strdup(&client->mudurl, mudurl); +} + +int sd_dhcp_client_set_user_class( + sd_dhcp_client *client, + char * const *user_class) { + + char **s = NULL; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(!strv_isempty(user_class), -EINVAL); + + STRV_FOREACH(p, user_class) { + size_t n = strlen(*p); + + if (n > 255 || n == 0) + return -EINVAL; + } + + s = strv_copy(user_class); + if (!s) + return -ENOMEM; + + return strv_free_and_replace(client->user_class, s); +} + +int sd_dhcp_client_set_client_port( + sd_dhcp_client *client, + uint16_t port) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + client->port = port; + + return 0; +} + +int sd_dhcp_client_set_mtu(sd_dhcp_client *client, uint32_t mtu) { + assert_return(client, -EINVAL); + assert_return(mtu >= DHCP_MIN_PACKET_SIZE, -ERANGE); + + /* MTU may be changed by the acquired lease. Hence, we cannot require that the client is stopped here. + * Please do not add assertion for !sd_dhcp_client_is_running(client) here. */ + + client->mtu = mtu; + + return 0; +} + +int sd_dhcp_client_set_max_attempts(sd_dhcp_client *client, uint64_t max_attempts) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + client->max_discover_attempts = max_attempts; + + return 0; +} + +int sd_dhcp_client_add_option(sd_dhcp_client *client, sd_dhcp_option *v) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(v, -EINVAL); + + r = ordered_hashmap_ensure_put(&client->extra_options, &dhcp_option_hash_ops, UINT_TO_PTR(v->option), v); + if (r < 0) + return r; + + sd_dhcp_option_ref(v); + return 0; +} + +int sd_dhcp_client_add_vendor_option(sd_dhcp_client *client, sd_dhcp_option *v) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(v, -EINVAL); + + r = ordered_hashmap_ensure_allocated(&client->vendor_options, &dhcp_option_hash_ops); + if (r < 0) + return -ENOMEM; + + r = ordered_hashmap_put(client->vendor_options, v, v); + if (r < 0) + return r; + + sd_dhcp_option_ref(v); + + return 1; +} + +int sd_dhcp_client_get_lease(sd_dhcp_client *client, sd_dhcp_lease **ret) { + assert_return(client, -EINVAL); + + if (!client->lease) + return -EADDRNOTAVAIL; + + if (ret) + *ret = client->lease; + + return 0; +} + +int sd_dhcp_client_set_service_type(sd_dhcp_client *client, int type) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + client->ip_service_type = type; + + return 0; +} + +int sd_dhcp_client_set_socket_priority(sd_dhcp_client *client, int socket_priority) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + client->socket_priority_set = true; + client->socket_priority = socket_priority; + + return 0; +} + +int sd_dhcp_client_set_fallback_lease_lifetime(sd_dhcp_client *client, uint64_t fallback_lease_lifetime) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + assert_return(fallback_lease_lifetime > 0, -EINVAL); + + assert_cc(sizeof(usec_t) == sizeof(uint64_t)); + client->fallback_lease_lifetime = fallback_lease_lifetime; + + return 0; +} + +static void client_set_state(sd_dhcp_client *client, DHCPState state) { + assert(client); + + if (client->state == state) + return; + + log_dhcp_client(client, "State changed: %s -> %s", + dhcp_state_to_string(client->state), dhcp_state_to_string(state)); + + client->state = state; + + if (client->state_callback) + client->state_callback(client, state, client->state_userdata); +} + +int dhcp_client_get_state(sd_dhcp_client *client) { + assert_return(client, -EINVAL); + + return client->state; +} + +static int client_notify(sd_dhcp_client *client, int event) { + assert(client); + + if (client->callback) + return client->callback(client, event, client->userdata); + + return 0; +} + +static int client_initialize(sd_dhcp_client *client) { + assert_return(client, -EINVAL); + + client->receive_message = sd_event_source_disable_unref(client->receive_message); + + client->fd = safe_close(client->fd); + + (void) event_source_disable(client->timeout_resend); + (void) event_source_disable(client->timeout_t1); + (void) event_source_disable(client->timeout_t2); + (void) event_source_disable(client->timeout_expire); + (void) event_source_disable(client->timeout_ipv6_only_mode); + + client->discover_attempt = 0; + client->request_attempt = 0; + + client_set_state(client, DHCP_STATE_STOPPED); + client->xid = 0; + + client->lease = sd_dhcp_lease_unref(client->lease); + + return 0; +} + +static void client_stop(sd_dhcp_client *client, int error) { + assert(client); + + if (error < 0) + log_dhcp_client_errno(client, error, "STOPPED: %m"); + else if (error == SD_DHCP_CLIENT_EVENT_STOP) + log_dhcp_client(client, "STOPPED"); + else + log_dhcp_client(client, "STOPPED: Unknown event"); + + client_notify(client, error); + + client_initialize(client); +} + +/* RFC2131 section 4.1: + * retransmission delays should include -1 to +1 sec of random 'fuzz'. */ +#define RFC2131_RANDOM_FUZZ \ + ((int64_t)(random_u64() % (2 * USEC_PER_SEC)) - (int64_t)USEC_PER_SEC) + +/* RFC2131 section 4.1: + * for retransmission delays, timeout should start at 4s then double + * each attempt with max of 64s, with -1 to +1 sec of random 'fuzz' added. + * This assumes the first call will be using attempt 1. */ +static usec_t client_compute_request_timeout(usec_t now, uint64_t attempt) { + usec_t timeout = (UINT64_C(1) << MIN(attempt + 1, UINT64_C(6))) * USEC_PER_SEC; + + return usec_sub_signed(usec_add(now, timeout), RFC2131_RANDOM_FUZZ); +} + +/* RFC2131 section 4.4.5: + * T1 defaults to (0.5 * duration_of_lease). + * T2 defaults to (0.875 * duration_of_lease). */ +#define T1_DEFAULT(lifetime) ((lifetime) / 2) +#define T2_DEFAULT(lifetime) (((lifetime) * 7) / 8) + +/* RFC2131 section 4.4.5: + * the client SHOULD wait one-half of the remaining time until T2 (in RENEWING state) + * and one-half of the remaining lease time (in REBINDING state), down to a minimum + * of 60 seconds. + * Note that while the default T1/T2 initial times do have random 'fuzz' applied, + * the RFC sec 4.4.5 does not mention adding any fuzz to retries. */ +static usec_t client_compute_reacquisition_timeout(usec_t now, usec_t expire) { + return now + MAX(usec_sub_unsigned(expire, now) / 2, 60 * USEC_PER_SEC); +} + +static int cmp_uint8(const uint8_t *a, const uint8_t *b) { + return CMP(*a, *b); +} + +static int client_message_init( + sd_dhcp_client *client, + DHCPPacket **ret, + uint8_t type, + size_t *_optlen, + size_t *_optoffset) { + + _cleanup_free_ DHCPPacket *packet = NULL; + size_t optlen, optoffset, size; + usec_t time_now; + uint16_t secs; + int r; + + assert(client); + assert(client->start_time); + assert(ret); + assert(_optlen); + assert(_optoffset); + assert(IN_SET(type, DHCP_DISCOVER, DHCP_REQUEST, DHCP_RELEASE, DHCP_DECLINE)); + + optlen = DHCP_MIN_OPTIONS_SIZE; + size = sizeof(DHCPPacket) + optlen; + + packet = malloc0(size); + if (!packet) + return -ENOMEM; + + r = dhcp_message_init(&packet->dhcp, BOOTREQUEST, client->xid, type, + client->arp_type, client->hw_addr.length, client->hw_addr.bytes, + optlen, &optoffset); + if (r < 0) + return r; + + /* Although 'secs' field is a SHOULD in RFC 2131, certain DHCP servers + refuse to issue an DHCP lease if 'secs' is set to zero */ + r = sd_event_now(client->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + return r; + assert(time_now >= client->start_time); + + /* seconds between sending first and last DISCOVER + * must always be strictly positive to deal with broken servers */ + secs = ((time_now - client->start_time) / USEC_PER_SEC) ?: 1; + packet->dhcp.secs = htobe16(secs); + + /* RFC2131 section 4.1 + A client that cannot receive unicast IP datagrams until its protocol + software has been configured with an IP address SHOULD set the + BROADCAST bit in the 'flags' field to 1 in any DHCPDISCOVER or + DHCPREQUEST messages that client sends. The BROADCAST bit will + provide a hint to the DHCP server and BOOTP relay agent to broadcast + any messages to the client on the client's subnet. + + Note: some interfaces needs this to be enabled, but some networks + needs this to be disabled as broadcasts are filteretd, so this + needs to be configurable */ + if (client->request_broadcast || client->arp_type != ARPHRD_ETHER) + packet->dhcp.flags = htobe16(0x8000); + + /* Some DHCP servers will refuse to issue an DHCP lease if the Client + Identifier option is not set */ + r = dhcp_option_append(&packet->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_CLIENT_IDENTIFIER, + client->client_id_len, + &client->client_id); + if (r < 0) + return r; + + /* RFC2131 section 3.5: + in its initial DHCPDISCOVER or DHCPREQUEST message, a + client may provide the server with a list of specific + parameters the client is interested in. If the client + includes a list of parameters in a DHCPDISCOVER message, + it MUST include that list in any subsequent DHCPREQUEST + messages. + */ + + /* RFC7844 section 3: + MAY contain the Parameter Request List option. */ + /* NOTE: in case that there would be an option to do not send + * any PRL at all, the size should be checked before sending */ + if (!set_isempty(client->req_opts) && type != DHCP_RELEASE) { + _cleanup_free_ uint8_t *opts = NULL; + size_t n_opts, i = 0; + void *val; + + n_opts = set_size(client->req_opts); + opts = new(uint8_t, n_opts); + if (!opts) + return -ENOMEM; + + SET_FOREACH(val, client->req_opts) + opts[i++] = PTR_TO_UINT8(val); + assert(i == n_opts); + + /* For anonymizing the request, let's sort the options. */ + typesafe_qsort(opts, n_opts, cmp_uint8); + + r = dhcp_option_append(&packet->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_PARAMETER_REQUEST_LIST, + n_opts, opts); + if (r < 0) + return r; + } + + /* RFC2131 section 3.5: + The client SHOULD include the ’maximum DHCP message size’ option to + let the server know how large the server may make its DHCP messages. + + Note (from ConnMan): Some DHCP servers will send bigger DHCP packets + than the defined default size unless the Maximum Message Size option + is explicitly set + + RFC3442 "Requirements to Avoid Sizing Constraints": + Because a full routing table can be quite large, the standard 576 + octet maximum size for a DHCP message may be too short to contain + some legitimate Classless Static Route options. Because of this, + clients implementing the Classless Static Route option SHOULD send a + Maximum DHCP Message Size [4] option if the DHCP client's TCP/IP + stack is capable of receiving larger IP datagrams. In this case, the + client SHOULD set the value of this option to at least the MTU of the + interface that the client is configuring. The client MAY set the + value of this option higher, up to the size of the largest UDP packet + it is prepared to accept. (Note that the value specified in the + Maximum DHCP Message Size option is the total maximum packet size, + including IP and UDP headers.) + */ + /* RFC7844 section 3: + SHOULD NOT contain any other option. */ + if (!client->anonymize && IN_SET(type, DHCP_DISCOVER, DHCP_REQUEST)) { + be16_t max_size = htobe16(MIN(client->mtu - DHCP_IP_UDP_SIZE, (uint32_t) UINT16_MAX)); + r = dhcp_option_append(&packet->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_MAXIMUM_MESSAGE_SIZE, + 2, &max_size); + if (r < 0) + return r; + } + + *_optlen = optlen; + *_optoffset = optoffset; + *ret = TAKE_PTR(packet); + + return 0; +} + +static int client_append_fqdn_option( + DHCPMessage *message, + size_t optlen, + size_t *optoffset, + const char *fqdn) { + + uint8_t buffer[3 + DHCP_MAX_FQDN_LENGTH]; + int r; + + buffer[0] = DHCP_FQDN_FLAG_S | /* Request server to perform A RR DNS updates */ + DHCP_FQDN_FLAG_E; /* Canonical wire format */ + buffer[1] = 0; /* RCODE1 (deprecated) */ + buffer[2] = 0; /* RCODE2 (deprecated) */ + + r = dns_name_to_wire_format(fqdn, buffer + 3, sizeof(buffer) - 3, false); + if (r > 0) + r = dhcp_option_append(message, optlen, optoffset, 0, + SD_DHCP_OPTION_FQDN, 3 + r, buffer); + + return r; +} + +static int dhcp_client_send_raw( + sd_dhcp_client *client, + DHCPPacket *packet, + size_t len) { + + dhcp_packet_append_ip_headers(packet, INADDR_ANY, client->port, + INADDR_BROADCAST, DHCP_PORT_SERVER, len, client->ip_service_type); + + return dhcp_network_send_raw_socket(client->fd, &client->link, + packet, len); +} + +static int client_append_common_discover_request_options(sd_dhcp_client *client, DHCPPacket *packet, size_t *optoffset, size_t optlen) { + sd_dhcp_option *j; + int r; + + assert(client); + + if (client->hostname) { + /* According to RFC 4702 "clients that send the Client FQDN option in + their messages MUST NOT also send the Host Name option". Just send + one of the two depending on the hostname type. + */ + if (dns_name_is_single_label(client->hostname)) { + /* it is unclear from RFC 2131 if client should send hostname in + DHCPDISCOVER but dhclient does and so we do as well + */ + r = dhcp_option_append(&packet->dhcp, optlen, optoffset, 0, + SD_DHCP_OPTION_HOST_NAME, + strlen(client->hostname), client->hostname); + } else + r = client_append_fqdn_option(&packet->dhcp, optlen, optoffset, + client->hostname); + if (r < 0) + return r; + } + + if (client->vendor_class_identifier) { + r = dhcp_option_append(&packet->dhcp, optlen, optoffset, 0, + SD_DHCP_OPTION_VENDOR_CLASS_IDENTIFIER, + strlen(client->vendor_class_identifier), + client->vendor_class_identifier); + if (r < 0) + return r; + } + + if (client->mudurl) { + r = dhcp_option_append(&packet->dhcp, optlen, optoffset, 0, + SD_DHCP_OPTION_MUD_URL, + strlen(client->mudurl), + client->mudurl); + if (r < 0) + return r; + } + + if (client->user_class) { + r = dhcp_option_append(&packet->dhcp, optlen, optoffset, 0, + SD_DHCP_OPTION_USER_CLASS, + strv_length(client->user_class), + client->user_class); + if (r < 0) + return r; + } + + ORDERED_HASHMAP_FOREACH(j, client->extra_options) { + r = dhcp_option_append(&packet->dhcp, optlen, optoffset, 0, + j->option, j->length, j->data); + if (r < 0) + return r; + } + + if (!ordered_hashmap_isempty(client->vendor_options)) { + r = dhcp_option_append( + &packet->dhcp, optlen, optoffset, 0, + SD_DHCP_OPTION_VENDOR_SPECIFIC, + ordered_hashmap_size(client->vendor_options), client->vendor_options); + if (r < 0) + return r; + } + + + return 0; +} + +static int client_send_discover(sd_dhcp_client *client) { + _cleanup_free_ DHCPPacket *discover = NULL; + size_t optoffset, optlen; + int r; + + assert(client); + assert(IN_SET(client->state, DHCP_STATE_INIT, DHCP_STATE_SELECTING)); + + r = client_message_init(client, &discover, DHCP_DISCOVER, + &optlen, &optoffset); + if (r < 0) + return r; + + /* the client may suggest values for the network address + and lease time in the DHCPDISCOVER message. The client may include + the ’requested IP address’ option to suggest that a particular IP + address be assigned, and may include the ’IP address lease time’ + option to suggest the lease time it would like. + */ + /* RFC7844 section 3: + SHOULD NOT contain any other option. */ + if (!client->anonymize && client->last_addr != INADDR_ANY) { + r = dhcp_option_append(&discover->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_REQUESTED_IP_ADDRESS, + 4, &client->last_addr); + if (r < 0) + return r; + } + + if (client->rapid_commit) { + r = dhcp_option_append(&discover->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_RAPID_COMMIT, 0, NULL); + if (r < 0) + return r; + } + + r = client_append_common_discover_request_options(client, discover, &optoffset, optlen); + if (r < 0) + return r; + + r = dhcp_option_append(&discover->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + /* We currently ignore: + The client SHOULD wait a random time between one and ten seconds to + desynchronize the use of DHCP at startup. + */ + r = dhcp_client_send_raw(client, discover, sizeof(DHCPPacket) + optoffset); + if (r < 0) + return r; + + log_dhcp_client(client, "DISCOVER"); + + return 0; +} + +static int client_send_request(sd_dhcp_client *client) { + _cleanup_free_ DHCPPacket *request = NULL; + size_t optoffset, optlen; + int r; + + assert(client); + + r = client_message_init(client, &request, DHCP_REQUEST, &optlen, &optoffset); + if (r < 0) + return r; + + switch (client->state) { + /* See RFC2131 section 4.3.2 (note that there is a typo in the RFC, + SELECTING should be REQUESTING) + */ + + case DHCP_STATE_REQUESTING: + /* Client inserts the address of the selected server in ’server + identifier’, ’ciaddr’ MUST be zero, ’requested IP address’ MUST be + filled in with the yiaddr value from the chosen DHCPOFFER. + */ + + r = dhcp_option_append(&request->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_SERVER_IDENTIFIER, + 4, &client->lease->server_address); + if (r < 0) + return r; + + r = dhcp_option_append(&request->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_REQUESTED_IP_ADDRESS, + 4, &client->lease->address); + if (r < 0) + return r; + + break; + + case DHCP_STATE_INIT_REBOOT: + /* ’server identifier’ MUST NOT be filled in, ’requested IP address’ + option MUST be filled in with client’s notion of its previously + assigned address. ’ciaddr’ MUST be zero. + */ + r = dhcp_option_append(&request->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_REQUESTED_IP_ADDRESS, + 4, &client->last_addr); + if (r < 0) + return r; + break; + + case DHCP_STATE_RENEWING: + /* ’server identifier’ MUST NOT be filled in, ’requested IP address’ + option MUST NOT be filled in, ’ciaddr’ MUST be filled in with + client’s IP address. + */ + + case DHCP_STATE_REBINDING: + /* ’server identifier’ MUST NOT be filled in, ’requested IP address’ + option MUST NOT be filled in, ’ciaddr’ MUST be filled in with + client’s IP address. + + This message MUST be broadcast to the 0xffffffff IP broadcast address. + */ + request->dhcp.ciaddr = client->lease->address; + + break; + + case DHCP_STATE_INIT: + case DHCP_STATE_SELECTING: + case DHCP_STATE_REBOOTING: + case DHCP_STATE_BOUND: + case DHCP_STATE_STOPPED: + default: + return -EINVAL; + } + + r = client_append_common_discover_request_options(client, request, &optoffset, optlen); + if (r < 0) + return r; + + r = dhcp_option_append(&request->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + if (client->state == DHCP_STATE_RENEWING) + r = dhcp_network_send_udp_socket(client->fd, + client->lease->server_address, + DHCP_PORT_SERVER, + &request->dhcp, + sizeof(DHCPMessage) + optoffset); + else + r = dhcp_client_send_raw(client, request, sizeof(DHCPPacket) + optoffset); + if (r < 0) + return r; + + switch (client->state) { + + case DHCP_STATE_REQUESTING: + log_dhcp_client(client, "REQUEST (requesting)"); + break; + + case DHCP_STATE_INIT_REBOOT: + log_dhcp_client(client, "REQUEST (init-reboot)"); + break; + + case DHCP_STATE_RENEWING: + log_dhcp_client(client, "REQUEST (renewing)"); + break; + + case DHCP_STATE_REBINDING: + log_dhcp_client(client, "REQUEST (rebinding)"); + break; + + default: + log_dhcp_client(client, "REQUEST (invalid)"); + break; + } + + return 0; +} + +static int client_start(sd_dhcp_client *client); + +static int client_timeout_resend( + sd_event_source *s, + uint64_t usec, + void *userdata) { + + sd_dhcp_client *client = ASSERT_PTR(userdata); + DHCP_CLIENT_DONT_DESTROY(client); + usec_t time_now, next_timeout; + int r; + + assert(s); + assert(client->event); + + r = sd_event_now(client->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + goto error; + + switch (client->state) { + + case DHCP_STATE_RENEWING: + next_timeout = client_compute_reacquisition_timeout(time_now, client->t2_time); + break; + + case DHCP_STATE_REBINDING: + next_timeout = client_compute_reacquisition_timeout(time_now, client->expire_time); + break; + + case DHCP_STATE_REBOOTING: + /* start over as we did not receive a timely ack or nak */ + r = client_initialize(client); + if (r < 0) + goto error; + + r = client_start(client); + if (r < 0) + goto error; + + log_dhcp_client(client, "REBOOTED"); + return 0; + + case DHCP_STATE_INIT: + case DHCP_STATE_INIT_REBOOT: + case DHCP_STATE_SELECTING: + if (client->discover_attempt >= client->max_discover_attempts) + goto error; + + client->discover_attempt++; + next_timeout = client_compute_request_timeout(time_now, client->discover_attempt); + break; + case DHCP_STATE_REQUESTING: + case DHCP_STATE_BOUND: + if (client->request_attempt >= client->max_request_attempts) + goto error; + + client->request_attempt++; + next_timeout = client_compute_request_timeout(time_now, client->request_attempt); + break; + + case DHCP_STATE_STOPPED: + r = -EINVAL; + goto error; + + default: + assert_not_reached(); + } + + r = event_reset_time(client->event, &client->timeout_resend, + CLOCK_BOOTTIME, + next_timeout, 10 * USEC_PER_MSEC, + client_timeout_resend, client, + client->event_priority, "dhcp4-resend-timer", true); + if (r < 0) + goto error; + + switch (client->state) { + case DHCP_STATE_INIT: + r = client_send_discover(client); + if (r >= 0) { + client_set_state(client, DHCP_STATE_SELECTING); + client->discover_attempt = 0; + } else if (client->discover_attempt >= client->max_discover_attempts) + goto error; + break; + + case DHCP_STATE_SELECTING: + r = client_send_discover(client); + if (r < 0 && client->discover_attempt >= client->max_discover_attempts) + goto error; + break; + + case DHCP_STATE_INIT_REBOOT: + case DHCP_STATE_REQUESTING: + case DHCP_STATE_RENEWING: + case DHCP_STATE_REBINDING: + r = client_send_request(client); + if (r < 0 && client->request_attempt >= client->max_request_attempts) + goto error; + + if (client->state == DHCP_STATE_INIT_REBOOT) + client_set_state(client, DHCP_STATE_REBOOTING); + break; + + case DHCP_STATE_REBOOTING: + case DHCP_STATE_BOUND: + break; + + case DHCP_STATE_STOPPED: + default: + r = -EINVAL; + goto error; + } + + if (client->discover_attempt >= TRANSIENT_FAILURE_ATTEMPTS) + client_notify(client, SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE); + + return 0; + +error: + /* Avoid REQUEST infinite loop. Per RFC 2131 section 3.1.5: if the client receives + neither a DHCPACK or a DHCPNAK message after employing the retransmission algorithm, + the client reverts to INIT state and restarts the initialization process */ + if (client->request_attempt >= client->max_request_attempts) { + log_dhcp_client(client, "Max REQUEST attempts reached. Restarting..."); + client_restart(client); + return 0; + } + client_stop(client, r); + + /* Errors were dealt with when stopping the client, don't spill + errors into the event loop handler */ + return 0; +} + +static int client_initialize_io_events( + sd_dhcp_client *client, + sd_event_io_handler_t io_callback) { + + int r; + + assert(client); + assert(client->event); + + r = sd_event_add_io(client->event, &client->receive_message, + client->fd, EPOLLIN, io_callback, + client); + if (r < 0) + goto error; + + r = sd_event_source_set_priority(client->receive_message, + client->event_priority); + if (r < 0) + goto error; + + r = sd_event_source_set_description(client->receive_message, "dhcp4-receive-message"); + if (r < 0) + goto error; + +error: + if (r < 0) + client_stop(client, r); + + return 0; +} + +static int client_initialize_time_events(sd_dhcp_client *client) { + usec_t usec = 0; + int r; + + assert(client); + assert(client->event); + + (void) event_source_disable(client->timeout_ipv6_only_mode); + + if (client->start_delay > 0) { + assert_se(sd_event_now(client->event, CLOCK_BOOTTIME, &usec) >= 0); + usec = usec_add(usec, client->start_delay); + } + + r = event_reset_time(client->event, &client->timeout_resend, + CLOCK_BOOTTIME, + usec, 0, + client_timeout_resend, client, + client->event_priority, "dhcp4-resend-timer", true); + if (r < 0) + client_stop(client, r); + + return 0; + +} + +static int client_initialize_events(sd_dhcp_client *client, sd_event_io_handler_t io_callback) { + client_initialize_io_events(client, io_callback); + client_initialize_time_events(client); + + return 0; +} + +static int client_start_delayed(sd_dhcp_client *client) { + int r; + + assert_return(client, -EINVAL); + assert_return(client->event, -EINVAL); + assert_return(client->ifindex > 0, -EINVAL); + assert_return(client->fd < 0, -EBUSY); + assert_return(client->xid == 0, -EINVAL); + assert_return(IN_SET(client->state, DHCP_STATE_STOPPED, DHCP_STATE_INIT_REBOOT), -EBUSY); + + client->xid = random_u32(); + + r = dhcp_network_bind_raw_socket(client->ifindex, &client->link, client->xid, + &client->hw_addr, &client->bcast_addr, + client->arp_type, client->port, + client->socket_priority_set, client->socket_priority); + if (r < 0) { + client_stop(client, r); + return r; + } + client->fd = r; + + client->start_time = now(CLOCK_BOOTTIME); + + if (client->state == DHCP_STATE_STOPPED) + client->state = DHCP_STATE_INIT; + + return client_initialize_events(client, client_receive_message_raw); +} + +static int client_start(sd_dhcp_client *client) { + client->start_delay = 0; + return client_start_delayed(client); +} + +static int client_timeout_expire(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp_client *client = userdata; + DHCP_CLIENT_DONT_DESTROY(client); + + log_dhcp_client(client, "EXPIRED"); + + client_notify(client, SD_DHCP_CLIENT_EVENT_EXPIRED); + + /* lease was lost, start over if not freed or stopped in callback */ + if (client->state != DHCP_STATE_STOPPED) { + client_initialize(client); + client_start(client); + } + + return 0; +} + +static int client_timeout_t2(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp_client *client = ASSERT_PTR(userdata); + DHCP_CLIENT_DONT_DESTROY(client); + int r; + + client->receive_message = sd_event_source_disable_unref(client->receive_message); + client->fd = safe_close(client->fd); + + client_set_state(client, DHCP_STATE_REBINDING); + client->discover_attempt = 0; + client->request_attempt = 0; + + r = dhcp_network_bind_raw_socket(client->ifindex, &client->link, client->xid, + &client->hw_addr, &client->bcast_addr, + client->arp_type, client->port, + client->socket_priority_set, client->socket_priority); + if (r < 0) { + client_stop(client, r); + return 0; + } + client->fd = r; + + return client_initialize_events(client, client_receive_message_raw); +} + +static int client_timeout_t1(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp_client *client = userdata; + DHCP_CLIENT_DONT_DESTROY(client); + + if (client->lease) + client_set_state(client, DHCP_STATE_RENEWING); + else if (client->state != DHCP_STATE_INIT) + client_set_state(client, DHCP_STATE_INIT_REBOOT); + client->discover_attempt = 0; + client->request_attempt = 0; + + return client_initialize_time_events(client); +} + +static int client_parse_message( + sd_dhcp_client *client, + DHCPMessage *message, + size_t len, + sd_dhcp_lease **ret) { + + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + _cleanup_free_ char *error_message = NULL; + int r; + + assert(client); + assert(message); + assert(ret); + + r = dhcp_lease_new(&lease); + if (r < 0) + return r; + + if (client->client_id_len > 0) { + r = dhcp_lease_set_client_id(lease, + (uint8_t *) &client->client_id, + client->client_id_len); + if (r < 0) + return r; + } + + r = dhcp_option_parse(message, len, dhcp_lease_parse_options, lease, &error_message); + if (r < 0) + return log_dhcp_client_errno(client, r, "Failed to parse DHCP options, ignoring: %m"); + + switch (client->state) { + case DHCP_STATE_SELECTING: + if (r == DHCP_ACK) { + if (!client->rapid_commit) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "received unexpected ACK, ignoring."); + if (!lease->rapid_commit) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "received rapid ACK without Rapid Commit option, ignoring."); + } else if (r == DHCP_OFFER) { + if (lease->rapid_commit) { + /* Some RFC incompliant servers provides an OFFER with a rapid commit option. + * See https://github.com/systemd/systemd/issues/29904. + * Let's support such servers gracefully. */ + log_dhcp_client(client, "received OFFER with Rapid Commit option, ignoring."); + lease->rapid_commit = false; + } + if (lease->lifetime == 0 && client->fallback_lease_lifetime > 0) + lease->lifetime = client->fallback_lease_lifetime; + } else + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "received unexpected message, ignoring."); + + break; + + case DHCP_STATE_REBOOTING: + case DHCP_STATE_REQUESTING: + case DHCP_STATE_RENEWING: + case DHCP_STATE_REBINDING: + if (r == DHCP_NAK) { + if (client->lease && client->lease->server_address != lease->server_address) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "NAK from unexpected server, ignoring: %s", + strna(error_message)); + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "NAK: %s", strna(error_message)); + } + if (r != DHCP_ACK) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "received message was not an ACK, ignoring."); + break; + + default: + assert_not_reached(); + } + + lease->next_server = message->siaddr; + lease->address = message->yiaddr; + + if (lease->address == 0 || + lease->server_address == 0 || + lease->lifetime == 0) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "received lease lacks address, server address or lease lifetime, ignoring."); + + r = dhcp_lease_set_default_subnet_mask(lease); + if (r < 0) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "received lease lacks subnet mask, and a fallback one cannot be generated, ignoring."); + + /* RFC 8925 section 3.2 + * If the client did not include the IPv6-Only Preferred option code in the Parameter Request List in + * the DHCPDISCOVER or DHCPREQUEST message, it MUST ignore the IPv6-Only Preferred option in any + * messages received from the server. */ + if (lease->ipv6_only_preferred_usec > 0 && + !client_request_contains(client, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED)) { + log_dhcp_client(client, "Received message with unrequested IPv6-only preferred option, ignoring the option."); + lease->ipv6_only_preferred_usec = 0; + } + + *ret = TAKE_PTR(lease); + return 0; +} + +static int client_handle_offer_or_rapid_ack(sd_dhcp_client *client, DHCPMessage *message, size_t len, const triple_timestamp *timestamp) { + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + int r; + + assert(client); + assert(message); + + r = client_parse_message(client, message, len, &lease); + if (r < 0) + return r; + + dhcp_lease_set_timestamp(lease, timestamp); + + dhcp_lease_unref_and_replace(client->lease, lease); + + if (client->lease->rapid_commit) { + log_dhcp_client(client, "ACK"); + return SD_DHCP_CLIENT_EVENT_IP_ACQUIRE; + } + + if (client_notify(client, SD_DHCP_CLIENT_EVENT_SELECTING) < 0) + return -ENOMSG; + + log_dhcp_client(client, "OFFER"); + return 0; +} + +static int client_enter_requesting_now(sd_dhcp_client *client) { + assert(client); + + client_set_state(client, DHCP_STATE_REQUESTING); + client->discover_attempt = 0; + client->request_attempt = 0; + + return event_reset_time(client->event, &client->timeout_resend, + CLOCK_BOOTTIME, 0, 0, + client_timeout_resend, client, + client->event_priority, "dhcp4-resend-timer", + /* force_reset = */ true); +} + +static int client_enter_requesting_delayed(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp_client *client = ASSERT_PTR(userdata); + DHCP_CLIENT_DONT_DESTROY(client); + int r; + + r = client_enter_requesting_now(client); + if (r < 0) + client_stop(client, r); + + return 0; +} + +static int client_enter_requesting(sd_dhcp_client *client) { + assert(client); + assert(client->lease); + + (void) event_source_disable(client->timeout_resend); + + if (client->lease->ipv6_only_preferred_usec > 0) { + if (client->ipv6_acquired) { + log_dhcp_client(client, + "Received an OFFER with IPv6-only preferred option, and the host already acquired IPv6 connectivity, stopping DHCPv4 client."); + return sd_dhcp_client_stop(client); + } + + log_dhcp_client(client, + "Received an OFFER with IPv6-only preferred option, delaying to send REQUEST with %s.", + FORMAT_TIMESPAN(client->lease->ipv6_only_preferred_usec, USEC_PER_SEC)); + + return event_reset_time_relative(client->event, &client->timeout_ipv6_only_mode, + CLOCK_BOOTTIME, + client->lease->ipv6_only_preferred_usec, 0, + client_enter_requesting_delayed, client, + client->event_priority, "dhcp4-ipv6-only-mode-timer", + /* force_reset = */ true); + } + + return client_enter_requesting_now(client); +} + +static int client_handle_forcerenew(sd_dhcp_client *client, DHCPMessage *force, size_t len) { + int r; + + r = dhcp_option_parse(force, len, NULL, NULL, NULL); + if (r != DHCP_FORCERENEW) + return -ENOMSG; + +#if 0 + log_dhcp_client(client, "FORCERENEW"); + return 0; +#else + /* FIXME: Ignore FORCERENEW requests until we implement RFC3118 (Authentication for DHCP + * Messages) and/or RFC6704 (Forcerenew Nonce Authentication), as unauthenticated FORCERENEW + * requests causes a security issue (TALOS-2020-1142, CVE-2020-13529). */ + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), + "Received FORCERENEW, ignoring."); +#endif +} + +static bool lease_equal(const sd_dhcp_lease *a, const sd_dhcp_lease *b) { + if (a->address != b->address) + return false; + + if (a->subnet_mask != b->subnet_mask) + return false; + + if (a->router_size != b->router_size) + return false; + + for (size_t i = 0; i < a->router_size; i++) + if (a->router[i].s_addr != b->router[i].s_addr) + return false; + + return true; +} + +static int client_handle_ack(sd_dhcp_client *client, DHCPMessage *message, size_t len, const triple_timestamp *timestamp) { + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + int r; + + assert(client); + assert(message); + + r = client_parse_message(client, message, len, &lease); + if (r < 0) + return r; + + dhcp_lease_set_timestamp(lease, timestamp); + + if (!client->lease) + r = SD_DHCP_CLIENT_EVENT_IP_ACQUIRE; + else if (lease_equal(client->lease, lease)) + r = SD_DHCP_CLIENT_EVENT_RENEW; + else + r = SD_DHCP_CLIENT_EVENT_IP_CHANGE; + + dhcp_lease_unref_and_replace(client->lease, lease); + + log_dhcp_client(client, "ACK"); + return r; +} + +static int client_set_lease_timeouts(sd_dhcp_client *client) { + usec_t time_now; + int r; + + assert(client); + assert(client->event); + assert(client->lease); + assert(client->lease->lifetime > 0); + assert(triple_timestamp_is_set(&client->lease->timestamp)); + + /* don't set timers for infinite leases */ + if (client->lease->lifetime == USEC_INFINITY) { + (void) event_source_disable(client->timeout_t1); + (void) event_source_disable(client->timeout_t2); + (void) event_source_disable(client->timeout_expire); + + return 0; + } + + r = sd_event_now(client->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + return r; + + /* verify that 0 < t2 < lifetime */ + if (client->lease->t2 == 0 || client->lease->t2 >= client->lease->lifetime) + client->lease->t2 = T2_DEFAULT(client->lease->lifetime); + /* verify that 0 < t1 < lifetime */ + if (client->lease->t1 == 0 || client->lease->t1 >= client->lease->t2) + client->lease->t1 = T1_DEFAULT(client->lease->lifetime); + /* now, if t1 >= t2, t1 *must* be T1_DEFAULT, since the previous check + * could not evaluate to false if t1 >= t2; so setting t2 to T2_DEFAULT + * guarantees t1 < t2. */ + if (client->lease->t1 >= client->lease->t2) + client->lease->t2 = T2_DEFAULT(client->lease->lifetime); + + assert(client->lease->t1 > 0); + assert(client->lease->t1 < client->lease->t2); + assert(client->lease->t2 < client->lease->lifetime); + + r = sd_dhcp_lease_get_lifetime_timestamp(client->lease, CLOCK_BOOTTIME, &client->expire_time); + if (r < 0) + return r; + r = sd_dhcp_lease_get_t1_timestamp(client->lease, CLOCK_BOOTTIME, &client->t1_time); + if (r < 0) + return r; + r = sd_dhcp_lease_get_t2_timestamp(client->lease, CLOCK_BOOTTIME, &client->t2_time); + if (r < 0) + return r; + + /* RFC2131 section 4.4.5: + * Times T1 and T2 SHOULD be chosen with some random "fuzz". + * Since the RFC doesn't specify here the exact 'fuzz' to use, + * we use the range from section 4.1: -1 to +1 sec. */ + client->t1_time = usec_sub_signed(client->t1_time, RFC2131_RANDOM_FUZZ); + client->t2_time = usec_sub_signed(client->t2_time, RFC2131_RANDOM_FUZZ); + + /* after fuzzing, ensure t2 is still >= t1 */ + client->t2_time = MAX(client->t1_time, client->t2_time); + + /* arm lifetime timeout */ + r = event_reset_time(client->event, &client->timeout_expire, + CLOCK_BOOTTIME, + client->expire_time, 10 * USEC_PER_MSEC, + client_timeout_expire, client, + client->event_priority, "dhcp4-lifetime", true); + if (r < 0) + return r; + + /* don't arm earlier timeouts if this has already expired */ + if (client->expire_time <= time_now) + return 0; + + log_dhcp_client(client, "lease expires in %s", + FORMAT_TIMESPAN(client->expire_time - time_now, USEC_PER_SEC)); + + /* arm T2 timeout */ + r = event_reset_time(client->event, &client->timeout_t2, + CLOCK_BOOTTIME, + client->t2_time, 10 * USEC_PER_MSEC, + client_timeout_t2, client, + client->event_priority, "dhcp4-t2-timeout", true); + if (r < 0) + return r; + + /* don't arm earlier timeout if this has already expired */ + if (client->t2_time <= time_now) + return 0; + + log_dhcp_client(client, "T2 expires in %s", + FORMAT_TIMESPAN(client->t2_time - time_now, USEC_PER_SEC)); + + /* arm T1 timeout */ + r = event_reset_time(client->event, &client->timeout_t1, + CLOCK_BOOTTIME, + client->t1_time, 10 * USEC_PER_MSEC, + client_timeout_t1, client, + client->event_priority, "dhcp4-t1-timer", true); + if (r < 0) + return r; + + if (client->t1_time > time_now) + log_dhcp_client(client, "T1 expires in %s", + FORMAT_TIMESPAN(client->t1_time - time_now, USEC_PER_SEC)); + + return 0; +} + +static int client_enter_bound_now(sd_dhcp_client *client, int notify_event) { + int r; + + assert(client); + + if (IN_SET(client->state, DHCP_STATE_REQUESTING, DHCP_STATE_REBOOTING)) + notify_event = SD_DHCP_CLIENT_EVENT_IP_ACQUIRE; + + client_set_state(client, DHCP_STATE_BOUND); + client->discover_attempt = 0; + client->request_attempt = 0; + + client->last_addr = client->lease->address; + + r = client_set_lease_timeouts(client); + if (r < 0) + log_dhcp_client_errno(client, r, "could not set lease timeouts: %m"); + + r = dhcp_network_bind_udp_socket(client->ifindex, client->lease->address, client->port, client->ip_service_type); + if (r < 0) + return log_dhcp_client_errno(client, r, "could not bind UDP socket: %m"); + + client->receive_message = sd_event_source_disable_unref(client->receive_message); + close_and_replace(client->fd, r); + client_initialize_io_events(client, client_receive_message_udp); + + client_notify(client, notify_event); + + return 0; +} + +static int client_enter_bound_delayed(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp_client *client = ASSERT_PTR(userdata); + DHCP_CLIENT_DONT_DESTROY(client); + int r; + + r = client_enter_bound_now(client, SD_DHCP_CLIENT_EVENT_IP_ACQUIRE); + if (r < 0) + client_stop(client, r); + + return 0; +} + +static int client_enter_bound(sd_dhcp_client *client, int notify_event) { + assert(client); + assert(client->lease); + + client->start_delay = 0; + (void) event_source_disable(client->timeout_resend); + + /* RFC 8925 section 3.2 + * If the client is in the INIT-REBOOT state, it SHOULD stop the DHCPv4 configuration process or + * disable the IPv4 stack completely for V6ONLY_WAIT seconds or until the network attachment event, + * whichever happens first. + * + * In the below, the condition uses REBOOTING, instead of INIT-REBOOT, as the client state has + * already transitioned from INIT-REBOOT to REBOOTING after sending a DHCPREQUEST message. */ + if (client->state == DHCP_STATE_REBOOTING && client->lease->ipv6_only_preferred_usec > 0) { + if (client->ipv6_acquired) { + log_dhcp_client(client, + "Received an ACK with IPv6-only preferred option, and the host already acquired IPv6 connectivity, stopping DHCPv4 client."); + return sd_dhcp_client_stop(client); + } + + log_dhcp_client(client, + "Received an ACK with IPv6-only preferred option, delaying to enter bound state with %s.", + FORMAT_TIMESPAN(client->lease->ipv6_only_preferred_usec, USEC_PER_SEC)); + + return event_reset_time_relative(client->event, &client->timeout_ipv6_only_mode, + CLOCK_BOOTTIME, + client->lease->ipv6_only_preferred_usec, 0, + client_enter_bound_delayed, client, + client->event_priority, "dhcp4-ipv6-only-mode", + /* force_reset = */ true); + } + + return client_enter_bound_now(client, notify_event); +} + +static int client_restart(sd_dhcp_client *client) { + int r; + assert(client); + + client_notify(client, SD_DHCP_CLIENT_EVENT_EXPIRED); + + r = client_initialize(client); + if (r < 0) + return r; + + r = client_start_delayed(client); + if (r < 0) + return r; + + log_dhcp_client(client, "REBOOT in %s", FORMAT_TIMESPAN(client->start_delay, USEC_PER_SEC)); + + client->start_delay = CLAMP(client->start_delay * 2, + RESTART_AFTER_NAK_MIN_USEC, RESTART_AFTER_NAK_MAX_USEC); + return 0; +} + +static int client_verify_message_header(sd_dhcp_client *client, DHCPMessage *message, size_t len) { + const uint8_t *expected_chaddr = NULL; + uint8_t expected_hlen = 0; + + assert(client); + assert(message); + + if (len < sizeof(DHCPMessage)) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Too small to be a DHCP message, ignoring."); + + if (be32toh(message->magic) != DHCP_MAGIC_COOKIE) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Not a DHCP message, ignoring."); + + if (message->op != BOOTREPLY) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Not a BOOTREPLY message, ignoring."); + + if (message->htype != client->arp_type) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Packet type does not match client type, ignoring."); + + if (client->arp_type == ARPHRD_ETHER) { + expected_hlen = ETH_ALEN; + expected_chaddr = client->hw_addr.bytes; + } + + if (message->hlen != expected_hlen) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Received packet hlen (%u) does not match expected (%u), ignoring.", + message->hlen, expected_hlen); + + if (memcmp_safe(message->chaddr, expected_chaddr, expected_hlen)) + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Received chaddr does not match expected, ignoring."); + + if (client->state != DHCP_STATE_BOUND && + be32toh(message->xid) != client->xid) + /* in BOUND state, we may receive FORCERENEW with xid set by server, + so ignore the xid in this case */ + return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(EBADMSG), + "Received xid (%u) does not match expected (%u), ignoring.", + be32toh(message->xid), client->xid); + + return 0; +} + +static int client_handle_message(sd_dhcp_client *client, DHCPMessage *message, size_t len, const triple_timestamp *timestamp) { + DHCP_CLIENT_DONT_DESTROY(client); + int r; + + assert(client); + assert(message); + assert(timestamp); + + if (client_verify_message_header(client, message, len) < 0) + return 0; + + switch (client->state) { + case DHCP_STATE_SELECTING: + + r = client_handle_offer_or_rapid_ack(client, message, len, timestamp); + if (ERRNO_IS_NEG_RESOURCE(r)) + return r; + if (r == -EADDRNOTAVAIL) + /* got a rapid NAK, let's restart the client */ + return client_restart(client); + if (r < 0) + return 0; /* invalid message, let's ignore it */ + + if (client->lease->rapid_commit) + /* got a successful rapid commit */ + return client_enter_bound(client, r); + + return client_enter_requesting(client); + + case DHCP_STATE_REBOOTING: + case DHCP_STATE_REQUESTING: + case DHCP_STATE_RENEWING: + case DHCP_STATE_REBINDING: + + r = client_handle_ack(client, message, len, timestamp); + if (ERRNO_IS_NEG_RESOURCE(r)) + return r; + if (r == -EADDRNOTAVAIL) + /* got a NAK, let's restart the client */ + return client_restart(client); + if (r < 0) + return 0; /* invalid message, let's ignore it */ + + return client_enter_bound(client, r); + + case DHCP_STATE_BOUND: + r = client_handle_forcerenew(client, message, len); + if (ERRNO_IS_NEG_RESOURCE(r)) + return r; + if (r < 0) + return 0; /* invalid message, let's ignore it */ + + return client_timeout_t1(NULL, 0, client); + + case DHCP_STATE_INIT: + case DHCP_STATE_INIT_REBOOT: + log_dhcp_client(client, "Unexpectedly receive message without sending any requests, ignoring."); + return 0; + + default: + assert_not_reached(); + } + + return 0; +} + +static int client_receive_message_udp( + sd_event_source *s, + int fd, + uint32_t revents, + void *userdata) { + + sd_dhcp_client *client = ASSERT_PTR(userdata); + _cleanup_free_ DHCPMessage *message = NULL; + ssize_t len, buflen; + /* This needs to be initialized with zero. See #20741. */ + CMSG_BUFFER_TYPE(CMSG_SPACE_TIMEVAL) control = {}; + struct iovec iov; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + int r; + + assert(s); + + buflen = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(buflen) || ERRNO_IS_NEG_DISCONNECT(buflen)) + return 0; + if (buflen < 0) { + log_dhcp_client_errno(client, buflen, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + message = malloc0(buflen); + if (!message) + return -ENOMEM; + + iov = IOVEC_MAKE(message, buflen); + + len = recvmsg_safe(fd, &msg, MSG_DONTWAIT); + if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len)) + return 0; + if (len < 0) { + log_dhcp_client_errno(client, len, "Could not receive message from UDP socket, ignoring: %m"); + return 0; + } + + log_dhcp_client(client, "Received message from UDP socket, processing."); + r = client_handle_message(client, message, len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg)); + if (r < 0) + client_stop(client, r); + + return 0; +} + +static int client_receive_message_raw( + sd_event_source *s, + int fd, + uint32_t revents, + void *userdata) { + + sd_dhcp_client *client = ASSERT_PTR(userdata); + _cleanup_free_ DHCPPacket *packet = NULL; + /* This needs to be initialized with zero. See #20741. */ + CMSG_BUFFER_TYPE(CMSG_SPACE_TIMEVAL + + CMSG_SPACE(sizeof(struct tpacket_auxdata))) control = {}; + struct iovec iov = {}; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + bool checksum = true; + ssize_t buflen, len; + int r; + + assert(s); + + buflen = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(buflen) || ERRNO_IS_NEG_DISCONNECT(buflen)) + return 0; + if (buflen < 0) { + log_dhcp_client_errno(client, buflen, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + packet = malloc0(buflen); + if (!packet) + return -ENOMEM; + + iov = IOVEC_MAKE(packet, buflen); + + len = recvmsg_safe(fd, &msg, 0); + if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len)) + return 0; + if (len < 0) { + log_dhcp_client_errno(client, len, "Could not receive message from raw socket, ignoring: %m"); + return 0; + } + + struct tpacket_auxdata *aux = CMSG_FIND_DATA(&msg, SOL_PACKET, PACKET_AUXDATA, struct tpacket_auxdata); + if (aux) + checksum = !(aux->tp_status & TP_STATUS_CSUMNOTREADY); + + if (dhcp_packet_verify_headers(packet, len, checksum, client->port) < 0) + return 0; + + len -= DHCP_IP_UDP_SIZE; + + log_dhcp_client(client, "Received message from RAW socket, processing."); + r = client_handle_message(client, &packet->dhcp, len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg)); + if (r < 0) + client_stop(client, r); + + return 0; +} + +int sd_dhcp_client_send_renew(sd_dhcp_client *client) { + assert_return(client, -EINVAL); + assert_return(sd_dhcp_client_is_running(client), -ESTALE); + assert_return(client->fd >= 0, -EINVAL); + + if (client->state != DHCP_STATE_BOUND) + return 0; + + assert(client->lease); + + client->start_delay = 0; + client->discover_attempt = 1; + client->request_attempt = 1; + client_set_state(client, DHCP_STATE_RENEWING); + + return client_initialize_time_events(client); +} + +int sd_dhcp_client_is_running(sd_dhcp_client *client) { + if (!client) + return 0; + + return client->state != DHCP_STATE_STOPPED; +} + +int sd_dhcp_client_start(sd_dhcp_client *client) { + int r; + + assert_return(client, -EINVAL); + + /* Note, do not reset the flag in client_initialize(), as it is also called on expire. */ + client->ipv6_acquired = false; + + r = client_initialize(client); + if (r < 0) + return r; + + /* If no client identifier exists, construct an RFC 4361-compliant one */ + if (client->client_id_len == 0) { + r = sd_dhcp_client_set_iaid_duid_en(client, /* iaid_set = */ false, /* iaid = */ 0); + if (r < 0) + return r; + } + + /* RFC7844 section 3.3: + SHOULD perform a complete four-way handshake, starting with a + DHCPDISCOVER, to obtain a new address lease. If the client can + ascertain that this is exactly the same network to which it was + previously connected, and if the link-layer address did not change, + the client MAY issue a DHCPREQUEST to try to reclaim the current + address. */ + if (client->last_addr && !client->anonymize) + client_set_state(client, DHCP_STATE_INIT_REBOOT); + + r = client_start(client); + if (r >= 0) + log_dhcp_client(client, "STARTED on ifindex %i", client->ifindex); + + return r; +} + +int sd_dhcp_client_send_release(sd_dhcp_client *client) { + assert_return(client, -EINVAL); + assert_return(sd_dhcp_client_is_running(client), -ESTALE); + assert_return(client->lease, -EUNATCH); + + _cleanup_free_ DHCPPacket *release = NULL; + size_t optoffset, optlen; + int r; + + r = client_message_init(client, &release, DHCP_RELEASE, &optlen, &optoffset); + if (r < 0) + return r; + + /* Fill up release IP and MAC */ + release->dhcp.ciaddr = client->lease->address; + memcpy(&release->dhcp.chaddr, client->hw_addr.bytes, client->hw_addr.length); + + r = dhcp_option_append(&release->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + r = dhcp_network_send_udp_socket(client->fd, + client->lease->server_address, + DHCP_PORT_SERVER, + &release->dhcp, + sizeof(DHCPMessage) + optoffset); + if (r < 0) + return r; + + log_dhcp_client(client, "RELEASE"); + + return 0; +} + +int sd_dhcp_client_send_decline(sd_dhcp_client *client) { + assert_return(client, -EINVAL); + assert_return(sd_dhcp_client_is_running(client), -ESTALE); + assert_return(client->lease, -EUNATCH); + + _cleanup_free_ DHCPPacket *release = NULL; + size_t optoffset, optlen; + int r; + + r = client_message_init(client, &release, DHCP_DECLINE, &optlen, &optoffset); + if (r < 0) + return r; + + release->dhcp.ciaddr = client->lease->address; + memcpy(&release->dhcp.chaddr, client->hw_addr.bytes, client->hw_addr.length); + + r = dhcp_option_append(&release->dhcp, optlen, &optoffset, 0, + SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + r = dhcp_network_send_udp_socket(client->fd, + client->lease->server_address, + DHCP_PORT_SERVER, + &release->dhcp, + sizeof(DHCPMessage) + optoffset); + if (r < 0) + return r; + + log_dhcp_client(client, "DECLINE"); + + client_stop(client, SD_DHCP_CLIENT_EVENT_STOP); + + if (client->state != DHCP_STATE_STOPPED) { + r = sd_dhcp_client_start(client); + if (r < 0) + return r; + } + + return 0; +} + +int sd_dhcp_client_stop(sd_dhcp_client *client) { + if (!client) + return 0; + + DHCP_CLIENT_DONT_DESTROY(client); + + client_stop(client, SD_DHCP_CLIENT_EVENT_STOP); + + return 0; +} + +int sd_dhcp_client_set_ipv6_connectivity(sd_dhcp_client *client, int have) { + if (!client) + return 0; + + /* We have already received a message with IPv6-Only preferred option, and are waiting for IPv6 + * connectivity or timeout, let's stop the client. */ + if (have && sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) > 0) + return sd_dhcp_client_stop(client); + + /* Otherwise, save that the host already has IPv6 connectivity. */ + client->ipv6_acquired = have; + return 0; +} + +int sd_dhcp_client_interrupt_ipv6_only_mode(sd_dhcp_client *client) { + assert_return(client, -EINVAL); + assert_return(sd_dhcp_client_is_running(client), -ESTALE); + assert_return(client->fd >= 0, -EINVAL); + + if (sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) <= 0) + return 0; + + client_initialize(client); + return client_start(client); +} + +int sd_dhcp_client_attach_event(sd_dhcp_client *client, sd_event *event, int64_t priority) { + int r; + + assert_return(client, -EINVAL); + assert_return(!client->event, -EBUSY); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + if (event) + client->event = sd_event_ref(event); + else { + r = sd_event_default(&client->event); + if (r < 0) + return 0; + } + + client->event_priority = priority; + + return 0; +} + +int sd_dhcp_client_detach_event(sd_dhcp_client *client) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp_client_is_running(client), -EBUSY); + + client->event = sd_event_unref(client->event); + + return 0; +} + +sd_event *sd_dhcp_client_get_event(sd_dhcp_client *client) { + assert_return(client, NULL); + + return client->event; +} + +int sd_dhcp_client_attach_device(sd_dhcp_client *client, sd_device *dev) { + assert_return(client, -EINVAL); + + return device_unref_and_replace(client->dev, dev); +} + +static sd_dhcp_client *dhcp_client_free(sd_dhcp_client *client) { + if (!client) + return NULL; + + log_dhcp_client(client, "FREE"); + + client_initialize(client); + + client->timeout_resend = sd_event_source_unref(client->timeout_resend); + client->timeout_t1 = sd_event_source_unref(client->timeout_t1); + client->timeout_t2 = sd_event_source_unref(client->timeout_t2); + client->timeout_expire = sd_event_source_unref(client->timeout_expire); + + sd_dhcp_client_detach_event(client); + + sd_device_unref(client->dev); + + set_free(client->req_opts); + free(client->hostname); + free(client->vendor_class_identifier); + free(client->mudurl); + client->user_class = strv_free(client->user_class); + ordered_hashmap_free(client->extra_options); + ordered_hashmap_free(client->vendor_options); + free(client->ifname); + return mfree(client); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_client, sd_dhcp_client, dhcp_client_free); + +int sd_dhcp_client_new(sd_dhcp_client **ret, int anonymize) { + const uint8_t *opts; + size_t n_opts; + int r; + + assert_return(ret, -EINVAL); + + _cleanup_(sd_dhcp_client_unrefp) sd_dhcp_client *client = new(sd_dhcp_client, 1); + if (!client) + return -ENOMEM; + + *client = (sd_dhcp_client) { + .n_ref = 1, + .state = DHCP_STATE_STOPPED, + .ifindex = -1, + .fd = -EBADF, + .mtu = DHCP_MIN_PACKET_SIZE, + .port = DHCP_PORT_CLIENT, + .anonymize = !!anonymize, + .max_discover_attempts = UINT64_MAX, + .max_request_attempts = 5, + .ip_service_type = -1, + }; + /* NOTE: this could be moved to a function. */ + if (anonymize) { + n_opts = ELEMENTSOF(default_req_opts_anonymize); + opts = default_req_opts_anonymize; + } else { + n_opts = ELEMENTSOF(default_req_opts); + opts = default_req_opts; + } + + for (size_t i = 0; i < n_opts; i++) { + r = sd_dhcp_client_set_request_option(client, opts[i]); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(client); + + return 0; +} + +static const char* const dhcp_state_table[_DHCP_STATE_MAX] = { + [DHCP_STATE_STOPPED] = "stopped", + [DHCP_STATE_INIT] = "initialization", + [DHCP_STATE_SELECTING] = "selecting", + [DHCP_STATE_INIT_REBOOT] = "init-reboot", + [DHCP_STATE_REBOOTING] = "rebooting", + [DHCP_STATE_REQUESTING] = "requesting", + [DHCP_STATE_BOUND] = "bound", + [DHCP_STATE_RENEWING] = "renewing", + [DHCP_STATE_REBINDING] = "rebinding", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(dhcp_state, DHCPState); diff --git a/src/libsystemd-network/sd-dhcp-lease.c b/src/libsystemd-network/sd-dhcp-lease.c new file mode 100644 index 0000000..4e3be98 --- /dev/null +++ b/src/libsystemd-network/sd-dhcp-lease.c @@ -0,0 +1,1607 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <arpa/inet.h> +#include <errno.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "sd-dhcp-lease.h" + +#include "alloc-util.h" +#include "dhcp-lease-internal.h" +#include "dhcp-option.h" +#include "dns-domain.h" +#include "env-file.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "hostname-util.h" +#include "in-addr-util.h" +#include "network-common.h" +#include "network-internal.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strv.h" +#include "time-util.h" +#include "tmpfile-util.h" +#include "unaligned.h" + +void dhcp_lease_set_timestamp(sd_dhcp_lease *lease, const triple_timestamp *timestamp) { + assert(lease); + + if (timestamp && triple_timestamp_is_set(timestamp)) + lease->timestamp = *timestamp; + else + triple_timestamp_now(&lease->timestamp); +} + +int sd_dhcp_lease_get_timestamp(sd_dhcp_lease *lease, clockid_t clock, uint64_t *ret) { + assert_return(lease, -EINVAL); + assert_return(TRIPLE_TIMESTAMP_HAS_CLOCK(clock), -EOPNOTSUPP); + assert_return(clock_supported(clock), -EOPNOTSUPP); + assert_return(ret, -EINVAL); + + if (!triple_timestamp_is_set(&lease->timestamp)) + return -ENODATA; + + *ret = triple_timestamp_by_clock(&lease->timestamp, clock); + return 0; +} + +int sd_dhcp_lease_get_address(sd_dhcp_lease *lease, struct in_addr *addr) { + assert_return(lease, -EINVAL); + assert_return(addr, -EINVAL); + + if (lease->address == 0) + return -ENODATA; + + addr->s_addr = lease->address; + return 0; +} + +int sd_dhcp_lease_get_broadcast(sd_dhcp_lease *lease, struct in_addr *addr) { + assert_return(lease, -EINVAL); + assert_return(addr, -EINVAL); + + if (!lease->have_broadcast) + return -ENODATA; + + addr->s_addr = lease->broadcast; + return 0; +} + +int sd_dhcp_lease_get_lifetime(sd_dhcp_lease *lease, uint64_t *ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (lease->lifetime <= 0) + return -ENODATA; + + *ret = lease->lifetime; + return 0; +} + +int sd_dhcp_lease_get_t1(sd_dhcp_lease *lease, uint64_t *ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (lease->t1 <= 0) + return -ENODATA; + + *ret = lease->t1; + return 0; +} + +int sd_dhcp_lease_get_t2(sd_dhcp_lease *lease, uint64_t *ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (lease->t2 <= 0) + return -ENODATA; + + *ret = lease->t2; + return 0; +} + +#define DEFINE_GET_TIMESTAMP(name) \ + int sd_dhcp_lease_get_##name##_timestamp( \ + sd_dhcp_lease *lease, \ + clockid_t clock, \ + uint64_t *ret) { \ + \ + usec_t t, timestamp; \ + int r; \ + \ + assert_return(ret, -EINVAL); \ + \ + r = sd_dhcp_lease_get_##name(lease, &t); \ + if (r < 0) \ + return r; \ + \ + r = sd_dhcp_lease_get_timestamp(lease, clock, ×tamp); \ + if (r < 0) \ + return r; \ + \ + *ret = usec_add(t, timestamp); \ + return 0; \ + } + +DEFINE_GET_TIMESTAMP(lifetime); +DEFINE_GET_TIMESTAMP(t1); +DEFINE_GET_TIMESTAMP(t2); + +int sd_dhcp_lease_get_mtu(sd_dhcp_lease *lease, uint16_t *mtu) { + assert_return(lease, -EINVAL); + assert_return(mtu, -EINVAL); + + if (lease->mtu <= 0) + return -ENODATA; + + *mtu = lease->mtu; + return 0; +} + +int sd_dhcp_lease_get_servers( + sd_dhcp_lease *lease, + sd_dhcp_lease_server_type_t what, + const struct in_addr **addr) { + + assert_return(lease, -EINVAL); + assert_return(what >= 0, -EINVAL); + assert_return(what < _SD_DHCP_LEASE_SERVER_TYPE_MAX, -EINVAL); + + if (lease->servers[what].size <= 0) + return -ENODATA; + + if (addr) + *addr = lease->servers[what].addr; + + return (int) lease->servers[what].size; +} + +int sd_dhcp_lease_get_dns(sd_dhcp_lease *lease, const struct in_addr **addr) { + return sd_dhcp_lease_get_servers(lease, SD_DHCP_LEASE_DNS, addr); +} +int sd_dhcp_lease_get_ntp(sd_dhcp_lease *lease, const struct in_addr **addr) { + return sd_dhcp_lease_get_servers(lease, SD_DHCP_LEASE_NTP, addr); +} +int sd_dhcp_lease_get_sip(sd_dhcp_lease *lease, const struct in_addr **addr) { + return sd_dhcp_lease_get_servers(lease, SD_DHCP_LEASE_SIP, addr); +} +int sd_dhcp_lease_get_pop3(sd_dhcp_lease *lease, const struct in_addr **addr) { + return sd_dhcp_lease_get_servers(lease, SD_DHCP_LEASE_POP3, addr); +} +int sd_dhcp_lease_get_smtp(sd_dhcp_lease *lease, const struct in_addr **addr) { + return sd_dhcp_lease_get_servers(lease, SD_DHCP_LEASE_SMTP, addr); +} +int sd_dhcp_lease_get_lpr(sd_dhcp_lease *lease, const struct in_addr **addr) { + return sd_dhcp_lease_get_servers(lease, SD_DHCP_LEASE_LPR, addr); +} + +int sd_dhcp_lease_get_domainname(sd_dhcp_lease *lease, const char **domainname) { + assert_return(lease, -EINVAL); + assert_return(domainname, -EINVAL); + + if (!lease->domainname) + return -ENODATA; + + *domainname = lease->domainname; + return 0; +} + +int sd_dhcp_lease_get_hostname(sd_dhcp_lease *lease, const char **hostname) { + assert_return(lease, -EINVAL); + assert_return(hostname, -EINVAL); + + if (!lease->hostname) + return -ENODATA; + + *hostname = lease->hostname; + return 0; +} + +int sd_dhcp_lease_get_root_path(sd_dhcp_lease *lease, const char **root_path) { + assert_return(lease, -EINVAL); + assert_return(root_path, -EINVAL); + + if (!lease->root_path) + return -ENODATA; + + *root_path = lease->root_path; + return 0; +} + +int sd_dhcp_lease_get_captive_portal(sd_dhcp_lease *lease, const char **ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (!lease->captive_portal) + return -ENODATA; + + *ret = lease->captive_portal; + return 0; +} + +int sd_dhcp_lease_get_router(sd_dhcp_lease *lease, const struct in_addr **addr) { + assert_return(lease, -EINVAL); + assert_return(addr, -EINVAL); + + if (lease->router_size <= 0) + return -ENODATA; + + *addr = lease->router; + return (int) lease->router_size; +} + +int sd_dhcp_lease_get_netmask(sd_dhcp_lease *lease, struct in_addr *addr) { + assert_return(lease, -EINVAL); + assert_return(addr, -EINVAL); + + if (!lease->have_subnet_mask) + return -ENODATA; + + addr->s_addr = lease->subnet_mask; + return 0; +} + +int sd_dhcp_lease_get_prefix(sd_dhcp_lease *lease, struct in_addr *ret_prefix, uint8_t *ret_prefixlen) { + struct in_addr address, netmask; + uint8_t prefixlen; + int r; + + assert_return(lease, -EINVAL); + + r = sd_dhcp_lease_get_address(lease, &address); + if (r < 0) + return r; + + r = sd_dhcp_lease_get_netmask(lease, &netmask); + if (r < 0) + return r; + + prefixlen = in4_addr_netmask_to_prefixlen(&netmask); + + r = in4_addr_mask(&address, prefixlen); + if (r < 0) + return r; + + if (ret_prefix) + *ret_prefix = address; + if (ret_prefixlen) + *ret_prefixlen = prefixlen; + return 0; +} + +int sd_dhcp_lease_get_server_identifier(sd_dhcp_lease *lease, struct in_addr *addr) { + assert_return(lease, -EINVAL); + assert_return(addr, -EINVAL); + + if (lease->server_address == 0) + return -ENODATA; + + addr->s_addr = lease->server_address; + return 0; +} + +int sd_dhcp_lease_get_next_server(sd_dhcp_lease *lease, struct in_addr *addr) { + assert_return(lease, -EINVAL); + assert_return(addr, -EINVAL); + + if (lease->next_server == 0) + return -ENODATA; + + addr->s_addr = lease->next_server; + return 0; +} + +/* + * The returned routes array must be freed by the caller. + * Route objects have the same lifetime of the lease and must not be freed. + */ +static int dhcp_lease_get_routes(sd_dhcp_route *routes, size_t n_routes, sd_dhcp_route ***ret) { + assert(routes || n_routes == 0); + + if (n_routes <= 0) + return -ENODATA; + + if (ret) { + sd_dhcp_route **buf; + + buf = new(sd_dhcp_route*, n_routes); + if (!buf) + return -ENOMEM; + + for (size_t i = 0; i < n_routes; i++) + buf[i] = &routes[i]; + + *ret = buf; + } + + return (int) n_routes; +} + +int sd_dhcp_lease_get_static_routes(sd_dhcp_lease *lease, sd_dhcp_route ***ret) { + assert_return(lease, -EINVAL); + + return dhcp_lease_get_routes(lease->static_routes, lease->n_static_routes, ret); +} + +int sd_dhcp_lease_get_classless_routes(sd_dhcp_lease *lease, sd_dhcp_route ***ret) { + assert_return(lease, -EINVAL); + + return dhcp_lease_get_routes(lease->classless_routes, lease->n_classless_routes, ret); +} + +int sd_dhcp_lease_get_search_domains(sd_dhcp_lease *lease, char ***domains) { + size_t r; + + assert_return(lease, -EINVAL); + assert_return(domains, -EINVAL); + + r = strv_length(lease->search_domains); + if (r > 0) { + *domains = lease->search_domains; + return (int) r; + } + + return -ENODATA; +} + +int sd_dhcp_lease_get_6rd( + sd_dhcp_lease *lease, + uint8_t *ret_ipv4masklen, + uint8_t *ret_prefixlen, + struct in6_addr *ret_prefix, + const struct in_addr **ret_br_addresses, + size_t *ret_n_br_addresses) { + + assert_return(lease, -EINVAL); + + if (lease->sixrd_n_br_addresses <= 0) + return -ENODATA; + + if (ret_ipv4masklen) + *ret_ipv4masklen = lease->sixrd_ipv4masklen; + if (ret_prefixlen) + *ret_prefixlen = lease->sixrd_prefixlen; + if (ret_prefix) + *ret_prefix = lease->sixrd_prefix; + if (ret_br_addresses) + *ret_br_addresses = lease->sixrd_br_addresses; + if (ret_n_br_addresses) + *ret_n_br_addresses = lease->sixrd_n_br_addresses; + + return 0; +} + +int sd_dhcp_lease_has_6rd(sd_dhcp_lease *lease) { + return lease && lease->sixrd_n_br_addresses > 0; +} + +int sd_dhcp_lease_get_vendor_specific(sd_dhcp_lease *lease, const void **data, size_t *data_len) { + assert_return(lease, -EINVAL); + assert_return(data, -EINVAL); + assert_return(data_len, -EINVAL); + + if (lease->vendor_specific_len <= 0) + return -ENODATA; + + *data = lease->vendor_specific; + *data_len = lease->vendor_specific_len; + return 0; +} + +static sd_dhcp_lease *dhcp_lease_free(sd_dhcp_lease *lease) { + struct sd_dhcp_raw_option *option; + + assert(lease); + + while ((option = LIST_POP(options, lease->private_options))) { + free(option->data); + free(option); + } + + free(lease->root_path); + free(lease->router); + free(lease->timezone); + free(lease->hostname); + free(lease->domainname); + free(lease->captive_portal); + + for (sd_dhcp_lease_server_type_t i = 0; i < _SD_DHCP_LEASE_SERVER_TYPE_MAX; i++) + free(lease->servers[i].addr); + + free(lease->static_routes); + free(lease->classless_routes); + free(lease->client_id); + free(lease->vendor_specific); + strv_free(lease->search_domains); + free(lease->sixrd_br_addresses); + return mfree(lease); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_lease, sd_dhcp_lease, dhcp_lease_free); + +static int lease_parse_be32_seconds(const uint8_t *option, size_t len, bool max_as_infinity, usec_t *ret) { + assert(option); + assert(ret); + + if (len != 4) + return -EINVAL; + + *ret = unaligned_be32_sec_to_usec(option, max_as_infinity); + return 0; +} + +static int lease_parse_u16(const uint8_t *option, size_t len, uint16_t *ret, uint16_t min) { + assert(option); + assert(ret); + + if (len != 2) + return -EINVAL; + + *ret = unaligned_read_be16((be16_t*) option); + if (*ret < min) + *ret = min; + + return 0; +} + +static int lease_parse_be32(const uint8_t *option, size_t len, be32_t *ret) { + assert(option); + assert(ret); + + if (len != 4) + return -EINVAL; + + memcpy(ret, option, 4); + return 0; +} + +static int lease_parse_domain(const uint8_t *option, size_t len, char **ret) { + _cleanup_free_ char *name = NULL, *normalized = NULL; + int r; + + assert(option); + assert(ret); + + r = dhcp_option_parse_string(option, len, &name); + if (r < 0) + return r; + if (!name) { + *ret = mfree(*ret); + return 0; + } + + r = dns_name_normalize(name, 0, &normalized); + if (r < 0) + return r; + + if (is_localhost(normalized)) + return -EINVAL; + + if (dns_name_is_root(normalized)) + return -EINVAL; + + free_and_replace(*ret, normalized); + + return 0; +} + +static int lease_parse_captive_portal(const uint8_t *option, size_t len, char **ret) { + _cleanup_free_ char *uri = NULL; + int r; + + assert(option); + assert(ret); + + r = dhcp_option_parse_string(option, len, &uri); + if (r < 0) + return r; + if (uri && !in_charset(uri, URI_VALID)) + return -EINVAL; + + return free_and_replace(*ret, uri); +} + +static int lease_parse_in_addrs(const uint8_t *option, size_t len, struct in_addr **ret, size_t *n_ret) { + assert(option || len == 0); + assert(ret); + assert(n_ret); + + if (len <= 0) { + *ret = mfree(*ret); + *n_ret = 0; + } else { + size_t n_addresses; + struct in_addr *addresses; + + if (len % 4 != 0) + return -EINVAL; + + n_addresses = len / 4; + + addresses = newdup(struct in_addr, option, n_addresses); + if (!addresses) + return -ENOMEM; + + free_and_replace(*ret, addresses); + *n_ret = n_addresses; + } + + return 0; +} + +static int lease_parse_sip_server(const uint8_t *option, size_t len, struct in_addr **ret, size_t *n_ret) { + assert(option || len == 0); + assert(ret); + assert(n_ret); + + if (len <= 0) + return -EINVAL; + + /* The SIP record is like the other, regular server records, but prefixed with a single "encoding" + * byte that is either 0 or 1. We only support it to be 1 for now. Let's drop it and parse it like + * the other fields */ + + if (option[0] != 1) { /* We only support IP address encoding for now */ + *ret = mfree(*ret); + *n_ret = 0; + return 0; + } + + return lease_parse_in_addrs(option + 1, len - 1, ret, n_ret); +} + +static int lease_parse_static_routes(sd_dhcp_lease *lease, const uint8_t *option, size_t len) { + int r; + + assert(lease); + assert(option || len <= 0); + + if (len % 8 != 0) + return -EINVAL; + + while (len >= 8) { + struct in_addr dst, gw; + uint8_t prefixlen; + + assert_se(lease_parse_be32(option, 4, &dst.s_addr) >= 0); + option += 4; + + assert_se(lease_parse_be32(option, 4, &gw.s_addr) >= 0); + option += 4; + + len -= 8; + + r = in4_addr_default_prefixlen(&dst, &prefixlen); + if (r < 0) { + log_debug("sd-dhcp-lease: cannot determine class of received static route, ignoring."); + continue; + } + + (void) in4_addr_mask(&dst, prefixlen); + + if (!GREEDY_REALLOC(lease->static_routes, lease->n_static_routes + 1)) + return -ENOMEM; + + lease->static_routes[lease->n_static_routes++] = (struct sd_dhcp_route) { + .dst_addr = dst, + .gw_addr = gw, + .dst_prefixlen = prefixlen, + }; + } + + return 0; +} + +/* parses RFC3442 Classless Static Route Option */ +static int lease_parse_classless_routes(sd_dhcp_lease *lease, const uint8_t *option, size_t len) { + assert(lease); + assert(option || len <= 0); + + /* option format: (subnet-mask-width significant-subnet-octets gateway-ip) */ + + while (len > 0) { + uint8_t prefixlen, dst_octets; + struct in_addr dst = {}, gw; + + prefixlen = *option; + option++; + len--; + + dst_octets = DIV_ROUND_UP(prefixlen, 8); + + /* can't have more than 4 octets in IPv4 */ + if (dst_octets > 4 || len < dst_octets) + return -EINVAL; + + memcpy(&dst, option, dst_octets); + option += dst_octets; + len -= dst_octets; + + if (len < 4) + return -EINVAL; + + assert_se(lease_parse_be32(option, 4, &gw.s_addr) >= 0); + option += 4; + len -= 4; + + if (!GREEDY_REALLOC(lease->classless_routes, lease->n_classless_routes + 1)) + return -ENOMEM; + + lease->classless_routes[lease->n_classless_routes++] = (struct sd_dhcp_route) { + .dst_addr = dst, + .gw_addr = gw, + .dst_prefixlen = prefixlen, + }; + } + + return 0; +} + +static int lease_parse_6rd(sd_dhcp_lease *lease, const uint8_t *option, size_t len) { + uint8_t ipv4masklen, prefixlen; + struct in6_addr prefix; + _cleanup_free_ struct in_addr *br_addresses = NULL; + size_t n_br_addresses; + + assert(lease); + assert(option); + + /* See RFC 5969 Section 7.1.1 */ + + if (lease->sixrd_n_br_addresses > 0) + /* Multiple 6rd option?? */ + return -EINVAL; + + /* option-length: The length of the DHCP option in octets (22 octets with one BR IPv4 address). */ + if (len < 2 + sizeof(struct in6_addr) + sizeof(struct in_addr) || + (len - 2 - sizeof(struct in6_addr)) % sizeof(struct in_addr) != 0) + return -EINVAL; + + /* IPv4MaskLen: The number of high-order bits that are identical across all CE IPv4 addresses + * within a given 6rd domain. This may be any value between 0 and 32. Any value + * greater than 32 is invalid. */ + ipv4masklen = option[0]; + if (ipv4masklen > 32) + return -EINVAL; + + /* 6rdPrefixLen: The IPv6 prefix length of the SP's 6rd IPv6 prefix in number of bits. For the + * purpose of bounds checking by DHCP option processing, the sum of + * (32 - IPv4MaskLen) + 6rdPrefixLen MUST be less than or equal to 128. */ + prefixlen = option[1]; + if (32 - ipv4masklen + prefixlen > 128) + return -EINVAL; + + /* 6rdPrefix: The service provider's 6rd IPv6 prefix represented as a 16-octet IPv6 address. + * The bits in the prefix after the 6rdPrefixlen number of bits are reserved and + * MUST be initialized to zero by the sender and ignored by the receiver. */ + memcpy(&prefix, option + 2, sizeof(struct in6_addr)); + (void) in6_addr_mask(&prefix, prefixlen); + + /* 6rdBRIPv4Address: One or more IPv4 addresses of the 6rd Border Relays for a given 6rd domain. */ + n_br_addresses = (len - 2 - sizeof(struct in6_addr)) / sizeof(struct in_addr); + br_addresses = newdup(struct in_addr, option + 2 + sizeof(struct in6_addr), n_br_addresses); + if (!br_addresses) + return -ENOMEM; + + lease->sixrd_ipv4masklen = ipv4masklen; + lease->sixrd_prefixlen = prefixlen; + lease->sixrd_prefix = prefix; + lease->sixrd_br_addresses = TAKE_PTR(br_addresses); + lease->sixrd_n_br_addresses = n_br_addresses; + + return 0; +} + +int dhcp_lease_parse_options(uint8_t code, uint8_t len, const void *option, void *userdata) { + sd_dhcp_lease *lease = ASSERT_PTR(userdata); + int r; + + switch (code) { + + case SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME: + r = lease_parse_be32_seconds(option, len, /* max_as_infinity = */ true, &lease->lifetime); + if (r < 0) + log_debug_errno(r, "Failed to parse lease time, ignoring: %m"); + + break; + + case SD_DHCP_OPTION_SERVER_IDENTIFIER: + r = lease_parse_be32(option, len, &lease->server_address); + if (r < 0) + log_debug_errno(r, "Failed to parse server identifier, ignoring: %m"); + + break; + + case SD_DHCP_OPTION_SUBNET_MASK: + r = lease_parse_be32(option, len, &lease->subnet_mask); + if (r < 0) + log_debug_errno(r, "Failed to parse subnet mask, ignoring: %m"); + else + lease->have_subnet_mask = true; + break; + + case SD_DHCP_OPTION_BROADCAST: + r = lease_parse_be32(option, len, &lease->broadcast); + if (r < 0) + log_debug_errno(r, "Failed to parse broadcast address, ignoring: %m"); + else + lease->have_broadcast = true; + break; + + case SD_DHCP_OPTION_ROUTER: + r = lease_parse_in_addrs(option, len, &lease->router, &lease->router_size); + if (r < 0) + log_debug_errno(r, "Failed to parse router addresses, ignoring: %m"); + break; + + case SD_DHCP_OPTION_RAPID_COMMIT: + if (len > 0) + log_debug("Invalid DHCP Rapid Commit option, ignoring."); + lease->rapid_commit = true; + break; + + case SD_DHCP_OPTION_DOMAIN_NAME_SERVER: + r = lease_parse_in_addrs(option, len, &lease->servers[SD_DHCP_LEASE_DNS].addr, &lease->servers[SD_DHCP_LEASE_DNS].size); + if (r < 0) + log_debug_errno(r, "Failed to parse DNS server, ignoring: %m"); + break; + + case SD_DHCP_OPTION_NTP_SERVER: + r = lease_parse_in_addrs(option, len, &lease->servers[SD_DHCP_LEASE_NTP].addr, &lease->servers[SD_DHCP_LEASE_NTP].size); + if (r < 0) + log_debug_errno(r, "Failed to parse NTP server, ignoring: %m"); + break; + + case SD_DHCP_OPTION_SIP_SERVER: + r = lease_parse_sip_server(option, len, &lease->servers[SD_DHCP_LEASE_SIP].addr, &lease->servers[SD_DHCP_LEASE_SIP].size); + if (r < 0) + log_debug_errno(r, "Failed to parse SIP server, ignoring: %m"); + break; + + case SD_DHCP_OPTION_POP3_SERVER: + r = lease_parse_in_addrs(option, len, &lease->servers[SD_DHCP_LEASE_POP3].addr, &lease->servers[SD_DHCP_LEASE_POP3].size); + if (r < 0) + log_debug_errno(r, "Failed to parse POP3 server, ignoring: %m"); + break; + + case SD_DHCP_OPTION_SMTP_SERVER: + r = lease_parse_in_addrs(option, len, &lease->servers[SD_DHCP_LEASE_SMTP].addr, &lease->servers[SD_DHCP_LEASE_SMTP].size); + if (r < 0) + log_debug_errno(r, "Failed to parse SMTP server, ignoring: %m"); + break; + + case SD_DHCP_OPTION_LPR_SERVER: + r = lease_parse_in_addrs(option, len, &lease->servers[SD_DHCP_LEASE_LPR].addr, &lease->servers[SD_DHCP_LEASE_LPR].size); + if (r < 0) + log_debug_errno(r, "Failed to parse LPR server, ignoring: %m"); + break; + + case SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL: + r = lease_parse_captive_portal(option, len, &lease->captive_portal); + if (r < 0) + log_debug_errno(r, "Failed to parse captive portal, ignoring: %m"); + break; + + case SD_DHCP_OPTION_STATIC_ROUTE: + r = lease_parse_static_routes(lease, option, len); + if (r < 0) + log_debug_errno(r, "Failed to parse static routes, ignoring: %m"); + break; + + case SD_DHCP_OPTION_MTU_INTERFACE: + r = lease_parse_u16(option, len, &lease->mtu, 68); + if (r < 0) + log_debug_errno(r, "Failed to parse MTU, ignoring: %m"); + if (lease->mtu < DHCP_MIN_PACKET_SIZE) { + log_debug("MTU value of %" PRIu16 " too small. Using default MTU value of %d instead.", lease->mtu, DHCP_MIN_PACKET_SIZE); + lease->mtu = DHCP_MIN_PACKET_SIZE; + } + + break; + + case SD_DHCP_OPTION_DOMAIN_NAME: + r = lease_parse_domain(option, len, &lease->domainname); + if (r < 0) { + log_debug_errno(r, "Failed to parse domain name, ignoring: %m"); + return 0; + } + + break; + + case SD_DHCP_OPTION_DOMAIN_SEARCH: + r = dhcp_lease_parse_search_domains(option, len, &lease->search_domains); + if (r < 0) + log_debug_errno(r, "Failed to parse Domain Search List, ignoring: %m"); + break; + + case SD_DHCP_OPTION_HOST_NAME: + r = lease_parse_domain(option, len, &lease->hostname); + if (r < 0) { + log_debug_errno(r, "Failed to parse hostname, ignoring: %m"); + return 0; + } + + break; + + case SD_DHCP_OPTION_ROOT_PATH: + r = dhcp_option_parse_string(option, len, &lease->root_path); + if (r < 0) + log_debug_errno(r, "Failed to parse root path, ignoring: %m"); + break; + + case SD_DHCP_OPTION_RENEWAL_TIME: + r = lease_parse_be32_seconds(option, len, /* max_as_infinity = */ true, &lease->t1); + if (r < 0) + log_debug_errno(r, "Failed to parse T1 time, ignoring: %m"); + break; + + case SD_DHCP_OPTION_REBINDING_TIME: + r = lease_parse_be32_seconds(option, len, /* max_as_infinity = */ true, &lease->t2); + if (r < 0) + log_debug_errno(r, "Failed to parse T2 time, ignoring: %m"); + break; + + case SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE: + r = lease_parse_classless_routes(lease, option, len); + if (r < 0) + log_debug_errno(r, "Failed to parse classless routes, ignoring: %m"); + break; + + case SD_DHCP_OPTION_TZDB_TIMEZONE: { + _cleanup_free_ char *tz = NULL; + + r = dhcp_option_parse_string(option, len, &tz); + if (r < 0) { + log_debug_errno(r, "Failed to parse timezone option, ignoring: %m"); + return 0; + } + + if (!timezone_is_valid(tz, LOG_DEBUG)) { + log_debug("Timezone is not valid, ignoring."); + return 0; + } + + free_and_replace(lease->timezone, tz); + + break; + } + + case SD_DHCP_OPTION_VENDOR_SPECIFIC: + + if (len <= 0) + lease->vendor_specific = mfree(lease->vendor_specific); + else { + void *p; + + p = memdup(option, len); + if (!p) + return -ENOMEM; + + free_and_replace(lease->vendor_specific, p); + } + + lease->vendor_specific_len = len; + break; + + case SD_DHCP_OPTION_6RD: + r = lease_parse_6rd(lease, option, len); + if (r < 0) + log_debug_errno(r, "Failed to parse 6rd option, ignoring: %m"); + break; + + case SD_DHCP_OPTION_IPV6_ONLY_PREFERRED: + r = lease_parse_be32_seconds(option, len, /* max_as_infinity = */ false, &lease->ipv6_only_preferred_usec); + if (r < 0) + log_debug_errno(r, "Failed to parse IPv6 only preferred option, ignoring: %m"); + + else if (lease->ipv6_only_preferred_usec < MIN_V6ONLY_WAIT_USEC && + !network_test_mode_enabled()) + lease->ipv6_only_preferred_usec = MIN_V6ONLY_WAIT_USEC; + break; + + case SD_DHCP_OPTION_PRIVATE_BASE ... SD_DHCP_OPTION_PRIVATE_LAST: + r = dhcp_lease_insert_private_option(lease, code, option, len); + if (r < 0) + return r; + + break; + + default: + log_debug("Ignoring DHCP option %"PRIu8" while parsing.", code); + break; + } + + return 0; +} + +/* Parses compressed domain names. */ +int dhcp_lease_parse_search_domains(const uint8_t *option, size_t len, char ***domains) { + _cleanup_strv_free_ char **names = NULL; + size_t pos = 0, cnt = 0; + int r; + + assert(domains); + assert(option || len == 0); + + if (len == 0) + return -EBADMSG; + + while (pos < len) { + _cleanup_free_ char *name = NULL; + size_t n = 0; + size_t jump_barrier = pos, next_chunk = 0; + bool first = true; + + for (;;) { + uint8_t c; + c = option[pos++]; + + if (c == 0) { + /* End of name */ + break; + } else if (c <= 63) { + const char *label; + + /* Literal label */ + label = (const char*) (option + pos); + pos += c; + if (pos >= len) + return -EBADMSG; + + if (!GREEDY_REALLOC(name, n + !first + DNS_LABEL_ESCAPED_MAX)) + return -ENOMEM; + + if (first) + first = false; + else + name[n++] = '.'; + + r = dns_label_escape(label, c, name + n, DNS_LABEL_ESCAPED_MAX); + if (r < 0) + return r; + + n += r; + } else if (FLAGS_SET(c, 0xc0)) { + /* Pointer */ + + uint8_t d; + uint16_t ptr; + + if (pos >= len) + return -EBADMSG; + + d = option[pos++]; + ptr = (uint16_t) (c & ~0xc0) << 8 | (uint16_t) d; + + /* Jumps are limited to a "prior occurrence" (RFC-1035 4.1.4) */ + if (ptr >= jump_barrier) + return -EBADMSG; + jump_barrier = ptr; + + /* Save current location so we don't end up re-parsing what's parsed so far. */ + if (next_chunk == 0) + next_chunk = pos; + + pos = ptr; + } else + return -EBADMSG; + } + + if (!GREEDY_REALLOC(name, n + 1)) + return -ENOMEM; + name[n] = 0; + + r = strv_extend(&names, name); + if (r < 0) + return r; + + cnt++; + + if (next_chunk != 0) + pos = next_chunk; + } + + strv_free_and_replace(*domains, names); + + return cnt; +} + +int dhcp_lease_insert_private_option(sd_dhcp_lease *lease, uint8_t tag, const void *data, uint8_t len) { + struct sd_dhcp_raw_option *option, *before = NULL; + + assert(lease); + + LIST_FOREACH(options, cur, lease->private_options) { + if (tag < cur->tag) { + before = cur; + break; + } + if (tag == cur->tag) { + log_debug("Ignoring duplicate option, tagged %i.", tag); + return 0; + } + } + + option = new(struct sd_dhcp_raw_option, 1); + if (!option) + return -ENOMEM; + + option->tag = tag; + option->length = len; + option->data = memdup(data, len); + if (!option->data) { + free(option); + return -ENOMEM; + } + + LIST_INSERT_BEFORE(options, lease->private_options, before, option); + return 0; +} + +int dhcp_lease_new(sd_dhcp_lease **ret) { + sd_dhcp_lease *lease; + + lease = new0(sd_dhcp_lease, 1); + if (!lease) + return -ENOMEM; + + lease->n_ref = 1; + + *ret = lease; + return 0; +} + +int dhcp_lease_save(sd_dhcp_lease *lease, const char *lease_file) { + _cleanup_(unlink_and_freep) char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + struct in_addr address; + const struct in_addr *addresses; + const void *client_id, *data; + size_t client_id_len, data_len; + const char *string; + uint16_t mtu; + _cleanup_free_ sd_dhcp_route **routes = NULL; + char **search_domains; + usec_t t; + int r; + + assert(lease); + assert(lease_file); + + r = fopen_temporary(lease_file, &f, &temp_path); + if (r < 0) + return r; + + (void) fchmod(fileno(f), 0644); + + fprintf(f, + "# This is private data. Do not parse.\n"); + + r = sd_dhcp_lease_get_address(lease, &address); + if (r >= 0) + fprintf(f, "ADDRESS=%s\n", IN4_ADDR_TO_STRING(&address)); + + r = sd_dhcp_lease_get_netmask(lease, &address); + if (r >= 0) + fprintf(f, "NETMASK=%s\n", IN4_ADDR_TO_STRING(&address)); + + r = sd_dhcp_lease_get_router(lease, &addresses); + if (r > 0) { + fputs("ROUTER=", f); + serialize_in_addrs(f, addresses, r, NULL, NULL); + fputc('\n', f); + } + + r = sd_dhcp_lease_get_server_identifier(lease, &address); + if (r >= 0) + fprintf(f, "SERVER_ADDRESS=%s\n", IN4_ADDR_TO_STRING(&address)); + + r = sd_dhcp_lease_get_next_server(lease, &address); + if (r >= 0) + fprintf(f, "NEXT_SERVER=%s\n", IN4_ADDR_TO_STRING(&address)); + + r = sd_dhcp_lease_get_broadcast(lease, &address); + if (r >= 0) + fprintf(f, "BROADCAST=%s\n", IN4_ADDR_TO_STRING(&address)); + + r = sd_dhcp_lease_get_mtu(lease, &mtu); + if (r >= 0) + fprintf(f, "MTU=%" PRIu16 "\n", mtu); + + r = sd_dhcp_lease_get_t1(lease, &t); + if (r >= 0) + fprintf(f, "T1=%s\n", FORMAT_TIMESPAN(t, USEC_PER_SEC)); + + r = sd_dhcp_lease_get_t2(lease, &t); + if (r >= 0) + fprintf(f, "T2=%s\n", FORMAT_TIMESPAN(t, USEC_PER_SEC)); + + r = sd_dhcp_lease_get_lifetime(lease, &t); + if (r >= 0) + fprintf(f, "LIFETIME=%s\n", FORMAT_TIMESPAN(t, USEC_PER_SEC)); + + r = sd_dhcp_lease_get_dns(lease, &addresses); + if (r > 0) { + fputs("DNS=", f); + serialize_in_addrs(f, addresses, r, NULL, NULL); + fputc('\n', f); + } + + r = sd_dhcp_lease_get_ntp(lease, &addresses); + if (r > 0) { + fputs("NTP=", f); + serialize_in_addrs(f, addresses, r, NULL, NULL); + fputc('\n', f); + } + + r = sd_dhcp_lease_get_sip(lease, &addresses); + if (r > 0) { + fputs("SIP=", f); + serialize_in_addrs(f, addresses, r, NULL, NULL); + fputc('\n', f); + } + + r = sd_dhcp_lease_get_domainname(lease, &string); + if (r >= 0) + fprintf(f, "DOMAINNAME=%s\n", string); + + r = sd_dhcp_lease_get_search_domains(lease, &search_domains); + if (r > 0) { + fputs("DOMAIN_SEARCH_LIST=", f); + fputstrv(f, search_domains, NULL, NULL); + fputc('\n', f); + } + + r = sd_dhcp_lease_get_hostname(lease, &string); + if (r >= 0) + fprintf(f, "HOSTNAME=%s\n", string); + + r = sd_dhcp_lease_get_root_path(lease, &string); + if (r >= 0) + fprintf(f, "ROOT_PATH=%s\n", string); + + r = sd_dhcp_lease_get_static_routes(lease, &routes); + if (r > 0) + serialize_dhcp_routes(f, "STATIC_ROUTES", routes, r); + + routes = mfree(routes); + r = sd_dhcp_lease_get_classless_routes(lease, &routes); + if (r > 0) + serialize_dhcp_routes(f, "CLASSLESS_ROUTES", routes, r); + + r = sd_dhcp_lease_get_timezone(lease, &string); + if (r >= 0) + fprintf(f, "TIMEZONE=%s\n", string); + + r = sd_dhcp_lease_get_client_id(lease, &client_id, &client_id_len); + if (r >= 0) { + _cleanup_free_ char *client_id_hex = NULL; + + client_id_hex = hexmem(client_id, client_id_len); + if (!client_id_hex) + return -ENOMEM; + fprintf(f, "CLIENTID=%s\n", client_id_hex); + } + + r = sd_dhcp_lease_get_vendor_specific(lease, &data, &data_len); + if (r >= 0) { + _cleanup_free_ char *option_hex = NULL; + + option_hex = hexmem(data, data_len); + if (!option_hex) + return -ENOMEM; + fprintf(f, "VENDOR_SPECIFIC=%s\n", option_hex); + } + + LIST_FOREACH(options, option, lease->private_options) { + char key[STRLEN("OPTION_000")+1]; + + xsprintf(key, "OPTION_%" PRIu8, option->tag); + r = serialize_dhcp_option(f, key, option->data, option->length); + if (r < 0) + return r; + } + + r = fflush_and_check(f); + if (r < 0) + return r; + + r = conservative_rename(temp_path, lease_file); + if (r < 0) + return r; + + temp_path = mfree(temp_path); + + return 0; +} + +static char **private_options_free(char **options) { + if (!options) + return NULL; + + free_many_charp(options, SD_DHCP_OPTION_PRIVATE_LAST - SD_DHCP_OPTION_PRIVATE_BASE + 1); + + return mfree(options); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(char**, private_options_free); + +int dhcp_lease_load(sd_dhcp_lease **ret, const char *lease_file) { + _cleanup_(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + _cleanup_free_ char + *address = NULL, + *router = NULL, + *netmask = NULL, + *server_address = NULL, + *next_server = NULL, + *broadcast = NULL, + *dns = NULL, + *ntp = NULL, + *sip = NULL, + *pop3 = NULL, + *smtp = NULL, + *lpr = NULL, + *mtu = NULL, + *static_routes = NULL, + *classless_routes = NULL, + *domains = NULL, + *client_id_hex = NULL, + *vendor_specific_hex = NULL, + *lifetime = NULL, + *t1 = NULL, + *t2 = NULL; + _cleanup_(private_options_freep) char **options = NULL; + + int r, i; + + assert(lease_file); + assert(ret); + + r = dhcp_lease_new(&lease); + if (r < 0) + return r; + + options = new0(char*, SD_DHCP_OPTION_PRIVATE_LAST - SD_DHCP_OPTION_PRIVATE_BASE + 1); + if (!options) + return -ENOMEM; + + r = parse_env_file(NULL, lease_file, + "ADDRESS", &address, + "ROUTER", &router, + "NETMASK", &netmask, + "SERVER_ADDRESS", &server_address, + "NEXT_SERVER", &next_server, + "BROADCAST", &broadcast, + "DNS", &dns, + "NTP", &ntp, + "SIP", &sip, + "POP3", &pop3, + "SMTP", &smtp, + "LPR", &lpr, + "MTU", &mtu, + "DOMAINNAME", &lease->domainname, + "HOSTNAME", &lease->hostname, + "DOMAIN_SEARCH_LIST", &domains, + "ROOT_PATH", &lease->root_path, + "STATIC_ROUTES", &static_routes, + "CLASSLESS_ROUTES", &classless_routes, + "CLIENTID", &client_id_hex, + "TIMEZONE", &lease->timezone, + "VENDOR_SPECIFIC", &vendor_specific_hex, + "LIFETIME", &lifetime, + "T1", &t1, + "T2", &t2, + "OPTION_224", &options[0], + "OPTION_225", &options[1], + "OPTION_226", &options[2], + "OPTION_227", &options[3], + "OPTION_228", &options[4], + "OPTION_229", &options[5], + "OPTION_230", &options[6], + "OPTION_231", &options[7], + "OPTION_232", &options[8], + "OPTION_233", &options[9], + "OPTION_234", &options[10], + "OPTION_235", &options[11], + "OPTION_236", &options[12], + "OPTION_237", &options[13], + "OPTION_238", &options[14], + "OPTION_239", &options[15], + "OPTION_240", &options[16], + "OPTION_241", &options[17], + "OPTION_242", &options[18], + "OPTION_243", &options[19], + "OPTION_244", &options[20], + "OPTION_245", &options[21], + "OPTION_246", &options[22], + "OPTION_247", &options[23], + "OPTION_248", &options[24], + "OPTION_249", &options[25], + "OPTION_250", &options[26], + "OPTION_251", &options[27], + "OPTION_252", &options[28], + "OPTION_253", &options[29], + "OPTION_254", &options[30]); + if (r < 0) + return r; + + if (address) { + r = inet_pton(AF_INET, address, &lease->address); + if (r <= 0) + log_debug("Failed to parse address %s, ignoring.", address); + } + + if (router) { + r = deserialize_in_addrs(&lease->router, router); + if (r < 0) + log_debug_errno(r, "Failed to deserialize router addresses %s, ignoring: %m", router); + else + lease->router_size = r; + } + + if (netmask) { + r = inet_pton(AF_INET, netmask, &lease->subnet_mask); + if (r <= 0) + log_debug("Failed to parse netmask %s, ignoring.", netmask); + else + lease->have_subnet_mask = true; + } + + if (server_address) { + r = inet_pton(AF_INET, server_address, &lease->server_address); + if (r <= 0) + log_debug("Failed to parse server address %s, ignoring.", server_address); + } + + if (next_server) { + r = inet_pton(AF_INET, next_server, &lease->next_server); + if (r <= 0) + log_debug("Failed to parse next server %s, ignoring.", next_server); + } + + if (broadcast) { + r = inet_pton(AF_INET, broadcast, &lease->broadcast); + if (r <= 0) + log_debug("Failed to parse broadcast address %s, ignoring.", broadcast); + else + lease->have_broadcast = true; + } + + if (dns) { + r = deserialize_in_addrs(&lease->servers[SD_DHCP_LEASE_DNS].addr, dns); + if (r < 0) + log_debug_errno(r, "Failed to deserialize DNS servers %s, ignoring: %m", dns); + else + lease->servers[SD_DHCP_LEASE_DNS].size = r; + } + + if (ntp) { + r = deserialize_in_addrs(&lease->servers[SD_DHCP_LEASE_NTP].addr, ntp); + if (r < 0) + log_debug_errno(r, "Failed to deserialize NTP servers %s, ignoring: %m", ntp); + else + lease->servers[SD_DHCP_LEASE_NTP].size = r; + } + + if (sip) { + r = deserialize_in_addrs(&lease->servers[SD_DHCP_LEASE_SIP].addr, sip); + if (r < 0) + log_debug_errno(r, "Failed to deserialize SIP servers %s, ignoring: %m", sip); + else + lease->servers[SD_DHCP_LEASE_SIP].size = r; + } + + if (pop3) { + r = deserialize_in_addrs(&lease->servers[SD_DHCP_LEASE_POP3].addr, pop3); + if (r < 0) + log_debug_errno(r, "Failed to deserialize POP3 server %s, ignoring: %m", pop3); + else + lease->servers[SD_DHCP_LEASE_POP3].size = r; + } + + if (smtp) { + r = deserialize_in_addrs(&lease->servers[SD_DHCP_LEASE_SMTP].addr, smtp); + if (r < 0) + log_debug_errno(r, "Failed to deserialize SMTP server %s, ignoring: %m", smtp); + else + lease->servers[SD_DHCP_LEASE_SMTP].size = r; + } + + if (lpr) { + r = deserialize_in_addrs(&lease->servers[SD_DHCP_LEASE_LPR].addr, lpr); + if (r < 0) + log_debug_errno(r, "Failed to deserialize LPR server %s, ignoring: %m", lpr); + else + lease->servers[SD_DHCP_LEASE_LPR].size = r; + } + + if (mtu) { + r = safe_atou16(mtu, &lease->mtu); + if (r < 0) + log_debug_errno(r, "Failed to parse MTU %s, ignoring: %m", mtu); + } + + if (domains) { + _cleanup_strv_free_ char **a = NULL; + a = strv_split(domains, " "); + if (!a) + return -ENOMEM; + + if (!strv_isempty(a)) + lease->search_domains = TAKE_PTR(a); + } + + if (static_routes) { + r = deserialize_dhcp_routes( + &lease->static_routes, + &lease->n_static_routes, + static_routes); + if (r < 0) + log_debug_errno(r, "Failed to parse DHCP static routes %s, ignoring: %m", static_routes); + } + + if (classless_routes) { + r = deserialize_dhcp_routes( + &lease->classless_routes, + &lease->n_classless_routes, + classless_routes); + if (r < 0) + log_debug_errno(r, "Failed to parse DHCP classless routes %s, ignoring: %m", classless_routes); + } + + if (lifetime) { + r = parse_sec(lifetime, &lease->lifetime); + if (r < 0) + log_debug_errno(r, "Failed to parse lifetime %s, ignoring: %m", lifetime); + } + + if (t1) { + r = parse_sec(t1, &lease->t1); + if (r < 0) + log_debug_errno(r, "Failed to parse T1 %s, ignoring: %m", t1); + } + + if (t2) { + r = parse_sec(t2, &lease->t2); + if (r < 0) + log_debug_errno(r, "Failed to parse T2 %s, ignoring: %m", t2); + } + + if (client_id_hex) { + r = unhexmem(client_id_hex, SIZE_MAX, &lease->client_id, &lease->client_id_len); + if (r < 0) + log_debug_errno(r, "Failed to parse client ID %s, ignoring: %m", client_id_hex); + } + + if (vendor_specific_hex) { + r = unhexmem(vendor_specific_hex, SIZE_MAX, &lease->vendor_specific, &lease->vendor_specific_len); + if (r < 0) + log_debug_errno(r, "Failed to parse vendor specific data %s, ignoring: %m", vendor_specific_hex); + } + + for (i = 0; i <= SD_DHCP_OPTION_PRIVATE_LAST - SD_DHCP_OPTION_PRIVATE_BASE; i++) { + _cleanup_free_ void *data = NULL; + size_t len; + + if (!options[i]) + continue; + + r = unhexmem(options[i], SIZE_MAX, &data, &len); + if (r < 0) { + log_debug_errno(r, "Failed to parse private DHCP option %s, ignoring: %m", options[i]); + continue; + } + + r = dhcp_lease_insert_private_option(lease, SD_DHCP_OPTION_PRIVATE_BASE + i, data, len); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(lease); + + return 0; +} + +int dhcp_lease_set_default_subnet_mask(sd_dhcp_lease *lease) { + struct in_addr address, mask; + int r; + + assert(lease); + + if (lease->have_subnet_mask) + return 0; + + if (lease->address == 0) + return -ENODATA; + + address.s_addr = lease->address; + + /* fall back to the default subnet masks based on address class */ + r = in4_addr_default_subnet_mask(&address, &mask); + if (r < 0) + return r; + + lease->subnet_mask = mask.s_addr; + lease->have_subnet_mask = true; + + return 0; +} + +int sd_dhcp_lease_get_client_id(sd_dhcp_lease *lease, const void **client_id, size_t *client_id_len) { + assert_return(lease, -EINVAL); + assert_return(client_id, -EINVAL); + assert_return(client_id_len, -EINVAL); + + if (!lease->client_id) + return -ENODATA; + + *client_id = lease->client_id; + *client_id_len = lease->client_id_len; + + return 0; +} + +int dhcp_lease_set_client_id(sd_dhcp_lease *lease, const void *client_id, size_t client_id_len) { + assert_return(lease, -EINVAL); + assert_return(client_id || client_id_len <= 0, -EINVAL); + + if (client_id_len <= 0) + lease->client_id = mfree(lease->client_id); + else { + void *p; + + p = memdup(client_id, client_id_len); + if (!p) + return -ENOMEM; + + free_and_replace(lease->client_id, p); + lease->client_id_len = client_id_len; + } + + return 0; +} + +int sd_dhcp_lease_get_timezone(sd_dhcp_lease *lease, const char **tz) { + assert_return(lease, -EINVAL); + assert_return(tz, -EINVAL); + + if (!lease->timezone) + return -ENODATA; + + *tz = lease->timezone; + return 0; +} + +int sd_dhcp_route_get_destination(sd_dhcp_route *route, struct in_addr *destination) { + assert_return(route, -EINVAL); + assert_return(destination, -EINVAL); + + *destination = route->dst_addr; + return 0; +} + +int sd_dhcp_route_get_destination_prefix_length(sd_dhcp_route *route, uint8_t *length) { + assert_return(route, -EINVAL); + assert_return(length, -EINVAL); + + *length = route->dst_prefixlen; + return 0; +} + +int sd_dhcp_route_get_gateway(sd_dhcp_route *route, struct in_addr *gateway) { + assert_return(route, -EINVAL); + assert_return(gateway, -EINVAL); + + *gateway = route->gw_addr; + return 0; +} diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c new file mode 100644 index 0000000..fcc5b74 --- /dev/null +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -0,0 +1,1792 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <net/if_arp.h> +#include <sys/ioctl.h> + +#include "sd-dhcp-server.h" +#include "sd-id128.h" + +#include "alloc-util.h" +#include "dhcp-network.h" +#include "dhcp-option.h" +#include "dhcp-packet.h" +#include "dhcp-server-internal.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "in-addr-util.h" +#include "iovec-util.h" +#include "memory-util.h" +#include "network-common.h" +#include "ordered-set.h" +#include "siphash24.h" +#include "string-util.h" +#include "unaligned.h" +#include "utf8.h" + +#define DHCP_DEFAULT_LEASE_TIME_USEC USEC_PER_HOUR +#define DHCP_MAX_LEASE_TIME_USEC (USEC_PER_HOUR*12) + +DHCPLease *dhcp_lease_free(DHCPLease *lease) { + if (!lease) + return NULL; + + if (lease->server) { + hashmap_remove_value(lease->server->bound_leases_by_address, UINT32_TO_PTR(lease->address), lease); + hashmap_remove_value(lease->server->bound_leases_by_client_id, &lease->client_id, lease); + hashmap_remove_value(lease->server->static_leases_by_address, UINT32_TO_PTR(lease->address), lease); + hashmap_remove_value(lease->server->static_leases_by_client_id, &lease->client_id, lease); + } + + free(lease->client_id.data); + free(lease->hostname); + return mfree(lease); +} + +/* configures the server's address and subnet, and optionally the pool's size and offset into the subnet + * the whole pool must fit into the subnet, and may not contain the first (any) nor last (broadcast) address + * moreover, the server's own address may be in the pool, and is in that case reserved in order not to + * accidentally hand it out */ +int sd_dhcp_server_configure_pool( + sd_dhcp_server *server, + const struct in_addr *address, + unsigned char prefixlen, + uint32_t offset, + uint32_t size) { + + struct in_addr netmask_addr; + be32_t netmask; + uint32_t server_off, broadcast_off, size_max; + + assert_return(server, -EINVAL); + assert_return(address, -EINVAL); + assert_return(address->s_addr != INADDR_ANY, -EINVAL); + assert_return(prefixlen <= 32, -ERANGE); + + assert_se(in4_addr_prefixlen_to_netmask(&netmask_addr, prefixlen)); + netmask = netmask_addr.s_addr; + + server_off = be32toh(address->s_addr & ~netmask); + broadcast_off = be32toh(~netmask); + + /* the server address cannot be the subnet address */ + assert_return(server_off != 0, -ERANGE); + + /* nor the broadcast address */ + assert_return(server_off != broadcast_off, -ERANGE); + + /* 0 offset means we should set a default, we skip the first (subnet) address + and take the next one */ + if (offset == 0) + offset = 1; + + size_max = (broadcast_off + 1) /* the number of addresses in the subnet */ + - offset /* exclude the addresses before the offset */ + - 1; /* exclude the last (broadcast) address */ + + /* The pool must contain at least one address */ + assert_return(size_max >= 1, -ERANGE); + + if (size != 0) + assert_return(size <= size_max, -ERANGE); + else + size = size_max; + + if (server->address != address->s_addr || server->netmask != netmask || server->pool_size != size || server->pool_offset != offset) { + + server->pool_offset = offset; + server->pool_size = size; + + server->address = address->s_addr; + server->netmask = netmask; + server->subnet = address->s_addr & netmask; + + /* Drop any leases associated with the old address range */ + hashmap_clear(server->bound_leases_by_address); + hashmap_clear(server->bound_leases_by_client_id); + + if (server->callback) + server->callback(server, SD_DHCP_SERVER_EVENT_LEASE_CHANGED, server->callback_userdata); + } + + return 0; +} + +int sd_dhcp_server_is_running(sd_dhcp_server *server) { + if (!server) + return false; + + return !!server->receive_message; +} + +int sd_dhcp_server_is_in_relay_mode(sd_dhcp_server *server) { + assert_return(server, -EINVAL); + + return in4_addr_is_set(&server->relay_target); +} + +void client_id_hash_func(const DHCPClientId *id, struct siphash *state) { + assert(id); + assert(id->length > 0); + assert(id->data); + + siphash24_compress(&id->length, sizeof(id->length), state); + siphash24_compress(id->data, id->length, state); +} + +int client_id_compare_func(const DHCPClientId *a, const DHCPClientId *b) { + int r; + + assert(a->length > 0); + assert(a->data); + assert(b->length > 0); + assert(b->data); + + r = CMP(a->length, b->length); + if (r != 0) + return r; + + return memcmp(a->data, b->data, a->length); +} + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + dhcp_lease_hash_ops, + DHCPClientId, + client_id_hash_func, + client_id_compare_func, + DHCPLease, + dhcp_lease_free); + +static sd_dhcp_server *dhcp_server_free(sd_dhcp_server *server) { + assert(server); + + sd_dhcp_server_stop(server); + + sd_event_unref(server->event); + + free(server->boot_server_name); + free(server->boot_filename); + free(server->timezone); + + for (sd_dhcp_lease_server_type_t i = 0; i < _SD_DHCP_LEASE_SERVER_TYPE_MAX; i++) + free(server->servers[i].addr); + + server->bound_leases_by_address = hashmap_free(server->bound_leases_by_address); + server->bound_leases_by_client_id = hashmap_free(server->bound_leases_by_client_id); + server->static_leases_by_address = hashmap_free(server->static_leases_by_address); + server->static_leases_by_client_id = hashmap_free(server->static_leases_by_client_id); + + ordered_set_free(server->extra_options); + ordered_set_free(server->vendor_options); + + free(server->agent_circuit_id); + free(server->agent_remote_id); + + free(server->ifname); + return mfree(server); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_server, sd_dhcp_server, dhcp_server_free); + +int sd_dhcp_server_new(sd_dhcp_server **ret, int ifindex) { + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; + + assert_return(ret, -EINVAL); + assert_return(ifindex > 0, -EINVAL); + + server = new(sd_dhcp_server, 1); + if (!server) + return -ENOMEM; + + *server = (sd_dhcp_server) { + .n_ref = 1, + .fd_raw = -EBADF, + .fd = -EBADF, + .fd_broadcast = -EBADF, + .address = htobe32(INADDR_ANY), + .netmask = htobe32(INADDR_ANY), + .ifindex = ifindex, + .bind_to_interface = true, + .default_lease_time = DHCP_DEFAULT_LEASE_TIME_USEC, + .max_lease_time = DHCP_MAX_LEASE_TIME_USEC, + .rapid_commit = true, + }; + + *ret = TAKE_PTR(server); + + return 0; +} + +int sd_dhcp_server_set_ifname(sd_dhcp_server *server, const char *ifname) { + assert_return(server, -EINVAL); + assert_return(ifname, -EINVAL); + + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) + return -EINVAL; + + return free_and_strdup(&server->ifname, ifname); +} + +int sd_dhcp_server_get_ifname(sd_dhcp_server *server, const char **ret) { + int r; + + assert_return(server, -EINVAL); + + r = get_ifname(server->ifindex, &server->ifname); + if (r < 0) + return r; + + if (ret) + *ret = server->ifname; + + return 0; +} + +int sd_dhcp_server_attach_event(sd_dhcp_server *server, sd_event *event, int64_t priority) { + int r; + + assert_return(server, -EINVAL); + assert_return(!server->event, -EBUSY); + + if (event) + server->event = sd_event_ref(event); + else { + r = sd_event_default(&server->event); + if (r < 0) + return r; + } + + server->event_priority = priority; + + return 0; +} + +int sd_dhcp_server_detach_event(sd_dhcp_server *server) { + assert_return(server, -EINVAL); + + server->event = sd_event_unref(server->event); + + return 0; +} + +sd_event *sd_dhcp_server_get_event(sd_dhcp_server *server) { + assert_return(server, NULL); + + return server->event; +} + +int sd_dhcp_server_set_boot_server_address(sd_dhcp_server *server, const struct in_addr *address) { + assert_return(server, -EINVAL); + + if (address) + server->boot_server_address = *address; + else + server->boot_server_address = (struct in_addr) {}; + + return 0; +} + +int sd_dhcp_server_set_boot_server_name(sd_dhcp_server *server, const char *name) { + int r; + + assert_return(server, -EINVAL); + + if (name) { + r = dns_name_is_valid(name); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + return free_and_strdup(&server->boot_server_name, name); +} + +int sd_dhcp_server_set_boot_filename(sd_dhcp_server *server, const char *filename) { + assert_return(server, -EINVAL); + + if (filename && (!string_is_safe(filename) || !ascii_is_valid(filename))) + return -EINVAL; + + return free_and_strdup(&server->boot_filename, filename); +} + +int sd_dhcp_server_stop(sd_dhcp_server *server) { + bool running; + + if (!server) + return 0; + + running = sd_dhcp_server_is_running(server); + + server->receive_message = sd_event_source_disable_unref(server->receive_message); + server->receive_broadcast = sd_event_source_disable_unref(server->receive_broadcast); + + server->fd_raw = safe_close(server->fd_raw); + server->fd = safe_close(server->fd); + server->fd_broadcast = safe_close(server->fd_broadcast); + + if (running) + log_dhcp_server(server, "STOPPED"); + + return 0; +} + +static bool dhcp_request_contains(DHCPRequest *req, uint8_t option) { + assert(req); + + if (!req->parameter_request_list) + return false; + + return memchr(req->parameter_request_list, option, req->parameter_request_list_len); +} + +static int dhcp_server_send_unicast_raw( + sd_dhcp_server *server, + uint8_t hlen, + const uint8_t *chaddr, + DHCPPacket *packet, + size_t len) { + + union sockaddr_union link = { + .ll.sll_family = AF_PACKET, + .ll.sll_protocol = htobe16(ETH_P_IP), + .ll.sll_ifindex = server->ifindex, + .ll.sll_halen = hlen, + }; + + assert(server); + assert(server->ifindex > 0); + assert(server->address != 0); + assert(hlen > 0); + assert(chaddr); + assert(packet); + assert(len > sizeof(DHCPPacket)); + + memcpy(link.ll.sll_addr, chaddr, hlen); + + if (len > UINT16_MAX) + return -EOVERFLOW; + + dhcp_packet_append_ip_headers(packet, server->address, DHCP_PORT_SERVER, + packet->dhcp.yiaddr, + DHCP_PORT_CLIENT, len, -1); + + return dhcp_network_send_raw_socket(server->fd_raw, &link, packet, len); +} + +static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination, + uint16_t destination_port, + DHCPMessage *message, size_t len) { + union sockaddr_union dest = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(destination_port), + .in.sin_addr.s_addr = destination, + }; + struct iovec iov = { + .iov_base = message, + .iov_len = len, + }; + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; + struct msghdr msg = { + .msg_name = &dest, + .msg_namelen = sizeof(dest.in), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + struct cmsghdr *cmsg; + struct in_pktinfo *pktinfo; + + assert(server); + assert(server->fd >= 0); + assert(message); + assert(len >= sizeof(DHCPMessage)); + + if (server->bind_to_interface) { + msg.msg_control = &control; + msg.msg_controllen = sizeof(control); + + cmsg = CMSG_FIRSTHDR(&msg); + assert(cmsg); + + cmsg->cmsg_level = IPPROTO_IP; + cmsg->cmsg_type = IP_PKTINFO; + cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); + + /* we attach source interface and address info to the message + rather than binding the socket. This will be mostly useful + when we gain support for arbitrary number of server addresses + */ + pktinfo = CMSG_TYPED_DATA(cmsg, struct in_pktinfo); + assert(pktinfo); + + pktinfo->ipi_ifindex = server->ifindex; + pktinfo->ipi_spec_dst.s_addr = server->address; + } + + if (sendmsg(server->fd, &msg, 0) < 0) + return -errno; + + return 0; +} + +static bool requested_broadcast(DHCPMessage *message) { + assert(message); + return message->flags & htobe16(0x8000); +} + +static int dhcp_server_send( + sd_dhcp_server *server, + uint8_t hlen, + const uint8_t *chaddr, + be32_t destination, + uint16_t destination_port, + DHCPPacket *packet, + size_t optoffset, + bool l2_broadcast) { + + if (destination != INADDR_ANY) + return dhcp_server_send_udp(server, destination, + destination_port, &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + else if (l2_broadcast) + return dhcp_server_send_udp(server, INADDR_BROADCAST, + destination_port, &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + else + /* we cannot send UDP packet to specific MAC address when the + address is not yet configured, so must fall back to raw + packets */ + return dhcp_server_send_unicast_raw(server, hlen, chaddr, packet, + sizeof(DHCPPacket) + optoffset); +} + +int dhcp_server_send_packet(sd_dhcp_server *server, + DHCPRequest *req, DHCPPacket *packet, + int type, size_t optoffset) { + be32_t destination = INADDR_ANY; + uint16_t destination_port = DHCP_PORT_CLIENT; + int r; + + assert(server); + assert(req); + assert(req->max_optlen > 0); + assert(req->message); + assert(optoffset <= req->max_optlen); + assert(packet); + + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, + SD_DHCP_OPTION_SERVER_IDENTIFIER, + 4, &server->address); + if (r < 0) + return r; + + if (req->agent_info_option) { + size_t opt_full_length = *(req->agent_info_option + 1) + 2; + /* there must be space left for SD_DHCP_OPTION_END */ + if (optoffset + opt_full_length < req->max_optlen) { + memcpy(packet->dhcp.options + optoffset, req->agent_info_option, opt_full_length); + optoffset += opt_full_length; + } + } + + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, + SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + /* RFC 2131 Section 4.1 + + If the ’giaddr’ field in a DHCP message from a client is non-zero, + the server sends any return messages to the ’DHCP server’ port on the + BOOTP relay agent whose address appears in ’giaddr’. If the ’giaddr’ + field is zero and the ’ciaddr’ field is nonzero, then the server + unicasts DHCPOFFER and DHCPACK messages to the address in ’ciaddr’. + If ’giaddr’ is zero and ’ciaddr’ is zero, and the broadcast bit is + set, then the server broadcasts DHCPOFFER and DHCPACK messages to + 0xffffffff. If the broadcast bit is not set and ’giaddr’ is zero and + ’ciaddr’ is zero, then the server unicasts DHCPOFFER and DHCPACK + messages to the client’s hardware address and ’yiaddr’ address. In + all cases, when ’giaddr’ is zero, the server broadcasts any DHCPNAK + messages to 0xffffffff. + + Section 4.3.2 + + If ’giaddr’ is set in the DHCPREQUEST message, the client is on a + different subnet. The server MUST set the broadcast bit in the + DHCPNAK, so that the relay agent will broadcast the DHCPNAK to the + client, because the client may not have a correct network address + or subnet mask, and the client may not be answering ARP requests. + */ + if (req->message->giaddr != 0) { + destination = req->message->giaddr; + destination_port = DHCP_PORT_SERVER; + if (type == DHCP_NAK) + packet->dhcp.flags = htobe16(0x8000); + } else if (req->message->ciaddr != 0 && type != DHCP_NAK) + destination = req->message->ciaddr; + + bool l2_broadcast = requested_broadcast(req->message) || type == DHCP_NAK; + return dhcp_server_send(server, req->message->hlen, req->message->chaddr, + destination, destination_port, packet, optoffset, l2_broadcast); +} + +static int server_message_init( + sd_dhcp_server *server, + DHCPPacket **ret, + uint8_t type, + size_t *ret_optoffset, + DHCPRequest *req) { + + _cleanup_free_ DHCPPacket *packet = NULL; + size_t optoffset = 0; + int r; + + assert(server); + assert(ret); + assert(ret_optoffset); + assert(IN_SET(type, DHCP_OFFER, DHCP_ACK, DHCP_NAK)); + assert(req); + + packet = malloc0(sizeof(DHCPPacket) + req->max_optlen); + if (!packet) + return -ENOMEM; + + r = dhcp_message_init(&packet->dhcp, BOOTREPLY, + be32toh(req->message->xid), type, + req->message->htype, req->message->hlen, req->message->chaddr, + req->max_optlen, &optoffset); + if (r < 0) + return r; + + packet->dhcp.flags = req->message->flags; + packet->dhcp.giaddr = req->message->giaddr; + + *ret_optoffset = optoffset; + *ret = TAKE_PTR(packet); + + return 0; +} + +static int server_send_offer_or_ack( + sd_dhcp_server *server, + DHCPRequest *req, + be32_t address, + uint8_t type) { + + static const uint8_t option_map[_SD_DHCP_LEASE_SERVER_TYPE_MAX] = { + [SD_DHCP_LEASE_DNS] = SD_DHCP_OPTION_DOMAIN_NAME_SERVER, + [SD_DHCP_LEASE_NTP] = SD_DHCP_OPTION_NTP_SERVER, + [SD_DHCP_LEASE_SIP] = SD_DHCP_OPTION_SIP_SERVER, + [SD_DHCP_LEASE_POP3] = SD_DHCP_OPTION_POP3_SERVER, + [SD_DHCP_LEASE_SMTP] = SD_DHCP_OPTION_SMTP_SERVER, + [SD_DHCP_LEASE_LPR] = SD_DHCP_OPTION_LPR_SERVER, + }; + + _cleanup_free_ DHCPPacket *packet = NULL; + sd_dhcp_option *j; + be32_t lease_time; + size_t offset; + int r; + + assert(server); + assert(req); + assert(IN_SET(type, DHCP_OFFER, DHCP_ACK)); + + r = server_message_init(server, &packet, type, &offset, req); + if (r < 0) + return r; + + packet->dhcp.yiaddr = address; + packet->dhcp.siaddr = server->boot_server_address.s_addr; + + lease_time = usec_to_be32_sec(req->lifetime); + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME, 4, + &lease_time); + if (r < 0) + return r; + + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_SUBNET_MASK, 4, &server->netmask); + if (r < 0) + return r; + + if (server->emit_router) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_ROUTER, 4, + in4_addr_is_set(&server->router_address) ? + &server->router_address.s_addr : + &server->address); + if (r < 0) + return r; + } + + if (server->boot_server_name) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_BOOT_SERVER_NAME, + strlen(server->boot_server_name), server->boot_server_name); + if (r < 0) + return r; + } + + if (server->boot_filename) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_BOOT_FILENAME, + strlen(server->boot_filename), server->boot_filename); + if (r < 0) + return r; + } + + for (sd_dhcp_lease_server_type_t k = 0; k < _SD_DHCP_LEASE_SERVER_TYPE_MAX; k++) { + if (server->servers[k].size <= 0) + continue; + + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + option_map[k], + sizeof(struct in_addr) * server->servers[k].size, + server->servers[k].addr); + if (r < 0) + return r; + } + + if (server->timezone) { + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_TZDB_TIMEZONE, + strlen(server->timezone), server->timezone); + if (r < 0) + return r; + } + + /* RFC 8925 section 3.3. DHCPv4 Server Behavior + * The server MUST NOT include the IPv6-Only Preferred option in the DHCPOFFER or DHCPACK message if + * the option was not present in the Parameter Request List sent by the client. */ + if (dhcp_request_contains(req, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED) && + server->ipv6_only_preferred_usec > 0) { + be32_t sec = usec_to_be32_sec(server->ipv6_only_preferred_usec); + + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_IPV6_ONLY_PREFERRED, + sizeof(sec), &sec); + if (r < 0) + return r; + } + + ORDERED_SET_FOREACH(j, server->extra_options) { + r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, + j->option, j->length, j->data); + if (r < 0) + return r; + } + + if (!ordered_set_isempty(server->vendor_options)) { + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_VENDOR_SPECIFIC, + ordered_set_size(server->vendor_options), server->vendor_options); + if (r < 0) + return r; + } + + if (server->rapid_commit && req->rapid_commit && type == DHCP_ACK) { + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_RAPID_COMMIT, + 0, NULL); + if (r < 0) + return r; + } + + return dhcp_server_send_packet(server, req, packet, type, offset); +} + +static int server_send_nak_or_ignore(sd_dhcp_server *server, bool init_reboot, DHCPRequest *req) { + _cleanup_free_ DHCPPacket *packet = NULL; + size_t offset; + int r; + + /* When a request is refused, RFC 2131, section 4.3.2 mentioned we should send NAK when the + * client is in INITREBOOT. If the client is in other state, there is nothing mentioned in the + * RFC whether we should send NAK or not. Hence, let's silently ignore the request. */ + + if (!init_reboot) + return 0; + + r = server_message_init(server, &packet, DHCP_NAK, &offset, req); + if (r < 0) + return log_dhcp_server_errno(server, r, "Failed to create NAK message: %m"); + + r = dhcp_server_send_packet(server, req, packet, DHCP_NAK, offset); + if (r < 0) + return log_dhcp_server_errno(server, r, "Could not send NAK message: %m"); + + log_dhcp_server(server, "NAK (0x%x)", be32toh(req->message->xid)); + return DHCP_NAK; +} + +static int server_send_forcerenew( + sd_dhcp_server *server, + be32_t address, + be32_t gateway, + uint8_t htype, + uint8_t hlen, + const uint8_t *chaddr) { + + _cleanup_free_ DHCPPacket *packet = NULL; + size_t optoffset = 0; + int r; + + assert(server); + assert(address != INADDR_ANY); + assert(chaddr); + + packet = malloc0(sizeof(DHCPPacket) + DHCP_MIN_OPTIONS_SIZE); + if (!packet) + return -ENOMEM; + + r = dhcp_message_init(&packet->dhcp, BOOTREPLY, 0, + DHCP_FORCERENEW, htype, hlen, chaddr, + DHCP_MIN_OPTIONS_SIZE, &optoffset); + if (r < 0) + return r; + + r = dhcp_option_append(&packet->dhcp, DHCP_MIN_OPTIONS_SIZE, + &optoffset, 0, SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + + return dhcp_server_send_udp(server, address, DHCP_PORT_CLIENT, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); +} + +static int parse_request(uint8_t code, uint8_t len, const void *option, void *userdata) { + DHCPRequest *req = ASSERT_PTR(userdata); + int r; + + switch (code) { + case SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME: + if (len == 4) + req->lifetime = unaligned_be32_sec_to_usec(option, /* max_as_infinity = */ true); + + break; + case SD_DHCP_OPTION_REQUESTED_IP_ADDRESS: + if (len == 4) + memcpy(&req->requested_ip, option, sizeof(be32_t)); + + break; + case SD_DHCP_OPTION_SERVER_IDENTIFIER: + if (len == 4) + memcpy(&req->server_id, option, sizeof(be32_t)); + + break; + case SD_DHCP_OPTION_CLIENT_IDENTIFIER: + if (len >= 2) { + uint8_t *data; + + data = memdup(option, len); + if (!data) + return -ENOMEM; + + free_and_replace(req->client_id.data, data); + req->client_id.length = len; + } + + break; + case SD_DHCP_OPTION_MAXIMUM_MESSAGE_SIZE: + + if (len == 2 && unaligned_read_be16(option) >= sizeof(DHCPPacket)) + req->max_optlen = unaligned_read_be16(option) - sizeof(DHCPPacket); + + break; + case SD_DHCP_OPTION_RELAY_AGENT_INFORMATION: + req->agent_info_option = (uint8_t*)option - 2; + + break; + case SD_DHCP_OPTION_HOST_NAME: + r = dhcp_option_parse_string(option, len, &req->hostname); + if (r < 0) { + log_debug_errno(r, "Failed to parse hostname, ignoring: %m"); + return 0; + } + + break; + case SD_DHCP_OPTION_PARAMETER_REQUEST_LIST: + req->parameter_request_list = option; + req->parameter_request_list_len = len; + break; + + case SD_DHCP_OPTION_RAPID_COMMIT: + req->rapid_commit = true; + break; + } + + return 0; +} + +static DHCPRequest* dhcp_request_free(DHCPRequest *req) { + if (!req) + return NULL; + + free(req->client_id.data); + free(req->hostname); + return mfree(req); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(DHCPRequest*, dhcp_request_free); + +static int ensure_sane_request(sd_dhcp_server *server, DHCPRequest *req, DHCPMessage *message) { + assert(req); + assert(message); + + req->message = message; + + if (message->hlen > sizeof(message->chaddr)) + return -EBADMSG; + + /* set client id based on MAC address if client did not send an explicit one */ + if (!req->client_id.data) { + uint8_t *data; + + if (message->hlen == 0) + return -EBADMSG; + + data = new0(uint8_t, message->hlen + 1); + if (!data) + return -ENOMEM; + + data[0] = 0x01; + memcpy(data + 1, message->chaddr, message->hlen); + + req->client_id.length = message->hlen + 1; + req->client_id.data = data; + } + + if (message->hlen == 0 || memeqzero(message->chaddr, message->hlen)) { + /* See RFC2131 section 4.1.1. + * hlen and chaddr may not be set for non-ethernet interface. + * Let's try to retrieve it from the client ID. */ + + if (!req->client_id.data) + return -EBADMSG; + + if (req->client_id.length <= 1 || req->client_id.length > sizeof(message->chaddr) + 1) + return -EBADMSG; + + if (req->client_id.data[0] != 0x01) + return -EBADMSG; + + message->hlen = req->client_id.length - 1; + memcpy(message->chaddr, req->client_id.data + 1, message->hlen); + } + + if (req->max_optlen < DHCP_MIN_OPTIONS_SIZE) + req->max_optlen = DHCP_MIN_OPTIONS_SIZE; + + if (req->lifetime <= 0) + req->lifetime = MAX(USEC_PER_SEC, server->default_lease_time); + + if (server->max_lease_time > 0 && req->lifetime > server->max_lease_time) + req->lifetime = server->max_lease_time; + + return 0; +} + +static void request_set_timestamp(DHCPRequest *req, const triple_timestamp *timestamp) { + assert(req); + + if (timestamp && triple_timestamp_is_set(timestamp)) + req->timestamp = *timestamp; + else + triple_timestamp_now(&req->timestamp); +} + +static int request_get_lifetime_timestamp(DHCPRequest *req, clockid_t clock, usec_t *ret) { + assert(req); + assert(TRIPLE_TIMESTAMP_HAS_CLOCK(clock)); + assert(clock_supported(clock)); + assert(ret); + + if (req->lifetime <= 0) + return -ENODATA; + + if (!triple_timestamp_is_set(&req->timestamp)) + return -ENODATA; + + *ret = usec_add(triple_timestamp_by_clock(&req->timestamp, clock), req->lifetime); + return 0; +} + +static bool address_is_in_pool(sd_dhcp_server *server, be32_t address) { + assert(server); + + if (server->pool_size == 0) + return false; + + if (address == server->address) + return false; + + if (be32toh(address) < (be32toh(server->subnet) | server->pool_offset) || + be32toh(address) >= (be32toh(server->subnet) | (server->pool_offset + server->pool_size))) + return false; + + if (hashmap_contains(server->static_leases_by_address, UINT32_TO_PTR(address))) + return false; + + return true; +} + +static int append_agent_information_option(sd_dhcp_server *server, DHCPMessage *message, size_t opt_length, size_t size) { + int r; + size_t offset; + + assert(server); + assert(message); + + r = dhcp_option_find_option(message->options, opt_length, SD_DHCP_OPTION_END, &offset); + if (r < 0) + return r; + + r = dhcp_option_append(message, size, &offset, 0, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION, 0, server); + if (r < 0) + return r; + + r = dhcp_option_append(message, size, &offset, 0, SD_DHCP_OPTION_END, 0, NULL); + if (r < 0) + return r; + return offset; +} + +static int dhcp_server_relay_message(sd_dhcp_server *server, DHCPMessage *message, size_t opt_length, size_t buflen) { + _cleanup_free_ DHCPPacket *packet = NULL; + int r; + + assert(server); + assert(message); + assert(sd_dhcp_server_is_in_relay_mode(server)); + + if (message->hlen == 0 || message->hlen > sizeof(message->chaddr) || memeqzero(message->chaddr, message->hlen)) + return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EBADMSG), + "(relay agent) received message without/invalid hardware address, discarding."); + + if (message->op == BOOTREQUEST) { + log_dhcp_server(server, "(relay agent) BOOTREQUEST (0x%x)", be32toh(message->xid)); + if (message->hops >= 16) + return -ETIME; + message->hops++; + + /* https://tools.ietf.org/html/rfc1542#section-4.1.1 */ + if (message->giaddr == 0) + message->giaddr = server->address; + + if (server->agent_circuit_id || server->agent_remote_id) { + r = append_agent_information_option(server, message, opt_length, buflen - sizeof(DHCPMessage)); + if (r < 0) + return log_dhcp_server_errno(server, r, "could not append relay option: %m"); + opt_length = r; + } + + return dhcp_server_send_udp(server, server->relay_target.s_addr, DHCP_PORT_SERVER, message, sizeof(DHCPMessage) + opt_length); + } else if (message->op == BOOTREPLY) { + log_dhcp_server(server, "(relay agent) BOOTREPLY (0x%x)", be32toh(message->xid)); + if (message->giaddr != server->address) + return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EBADMSG), + "(relay agent) BOOTREPLY giaddr mismatch, discarding"); + + int message_type = dhcp_option_parse(message, sizeof(DHCPMessage) + opt_length, NULL, NULL, NULL); + if (message_type < 0) + return message_type; + + packet = malloc0(sizeof(DHCPPacket) + opt_length); + if (!packet) + return -ENOMEM; + memcpy(&packet->dhcp, message, sizeof(DHCPMessage) + opt_length); + + r = dhcp_option_remove_option(packet->dhcp.options, opt_length, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION); + if (r > 0) + opt_length = r; + + bool l2_broadcast = requested_broadcast(message) || message_type == DHCP_NAK; + const be32_t destination = message_type == DHCP_NAK ? INADDR_ANY : message->ciaddr; + return dhcp_server_send(server, message->hlen, message->chaddr, destination, DHCP_PORT_CLIENT, packet, opt_length, l2_broadcast); + } + return -EBADMSG; +} + +static int prepare_new_lease(DHCPLease **ret_lease, be32_t address, DHCPRequest *req, usec_t expiration) { + _cleanup_(dhcp_lease_freep) DHCPLease *lease = NULL; + + assert(ret_lease); + assert(address != 0); + assert(req); + assert(expiration != 0); + + lease = new(DHCPLease, 1); + if (!lease) + return -ENOMEM; + + *lease = (DHCPLease) { + .address = address, + .client_id.length = req->client_id.length, + .htype = req->message->htype, + .hlen = req->message->hlen, + .gateway = req->message->giaddr, + .expiration = expiration, + }; + lease->client_id.data = memdup(req->client_id.data, req->client_id.length); + if (!lease->client_id.data) + return -ENOMEM; + + memcpy(lease->chaddr, req->message->chaddr, req->message->hlen); + + if (req->hostname) { + lease->hostname = strdup(req->hostname); + if (!lease->hostname) + return -ENOMEM; + } + + *ret_lease = TAKE_PTR(lease); + + return 0; +} + +static int server_ack_request(sd_dhcp_server *server, DHCPRequest *req, DHCPLease *existing_lease, be32_t address) { + usec_t expiration; + int r; + + assert(server); + assert(req); + assert(address != 0); + + r = request_get_lifetime_timestamp(req, CLOCK_BOOTTIME, &expiration); + if (r < 0) + return r; + + if (existing_lease) { + assert(existing_lease->server); + assert(existing_lease->address == address); + existing_lease->expiration = expiration; + + } else { + _cleanup_(dhcp_lease_freep) DHCPLease *lease = NULL; + + r = prepare_new_lease(&lease, address, req, expiration); + if (r < 0) + return log_dhcp_server_errno(server, r, "Failed to create new lease: %m"); + + lease->server = server; /* This must be set just before hashmap_put(). */ + + r = hashmap_ensure_put(&server->bound_leases_by_client_id, &dhcp_lease_hash_ops, &lease->client_id, lease); + if (r < 0) + return log_dhcp_server_errno(server, r, "Could not save lease: %m"); + + r = hashmap_ensure_put(&server->bound_leases_by_address, NULL, UINT32_TO_PTR(lease->address), lease); + if (r < 0) + return log_dhcp_server_errno(server, r, "Could not save lease: %m"); + + TAKE_PTR(lease); + } + + r = server_send_offer_or_ack(server, req, address, DHCP_ACK); + if (r < 0) + return log_dhcp_server_errno(server, r, "Could not send ACK: %m"); + + log_dhcp_server(server, "ACK (0x%x)", be32toh(req->message->xid)); + + if (server->callback) + server->callback(server, SD_DHCP_SERVER_EVENT_LEASE_CHANGED, server->callback_userdata); + + return DHCP_ACK; +} + +static int dhcp_server_cleanup_expired_leases(sd_dhcp_server *server) { + DHCPLease *lease; + usec_t time_now; + int r; + + assert(server); + + r = sd_event_now(server->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + return r; + + HASHMAP_FOREACH(lease, server->bound_leases_by_client_id) + if (lease->expiration < time_now) { + log_dhcp_server(server, "CLEAN (0x%x)", be32toh(lease->address)); + dhcp_lease_free(lease); + } + + return 0; +} + +static bool address_available(sd_dhcp_server *server, be32_t address) { + assert(server); + + if (hashmap_contains(server->bound_leases_by_address, UINT32_TO_PTR(address)) || + hashmap_contains(server->static_leases_by_address, UINT32_TO_PTR(address)) || + address == server->address) + return false; + + return true; +} + +static int server_get_static_lease(sd_dhcp_server *server, const DHCPRequest *req, DHCPLease **ret) { + DHCPLease *static_lease; + _cleanup_free_ uint8_t *data = NULL; + + assert(server); + assert(req); + assert(ret); + + static_lease = hashmap_get(server->static_leases_by_client_id, &req->client_id); + if (static_lease) { + *ret = static_lease; + return 0; + } + + /* when no lease is found based on the client id fall back to chaddr */ + data = new(uint8_t, req->message->hlen + 1); + if (!data) + return -ENOMEM; + + /* set client id type to 1: Ethernet Link-Layer (RFC 2132) */ + data[0] = 0x01; + memcpy(data + 1, req->message->chaddr, req->message->hlen); + + static_lease = hashmap_get(server->static_leases_by_client_id, + &(DHCPClientId) { + .length = req->message->hlen + 1, + .data = data, + }); + + *ret = static_lease; + + return 0; +} + +#define HASH_KEY SD_ID128_MAKE(0d,1d,fe,bd,f1,24,bd,b3,47,f1,dd,6e,73,21,93,30) + +int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, size_t length, const triple_timestamp *timestamp) { + _cleanup_(dhcp_request_freep) DHCPRequest *req = NULL; + _cleanup_free_ char *error_message = NULL; + DHCPLease *existing_lease, *static_lease; + int type, r; + + assert(server); + assert(message); + + if (message->op != BOOTREQUEST) + return 0; + + req = new0(DHCPRequest, 1); + if (!req) + return -ENOMEM; + + type = dhcp_option_parse(message, length, parse_request, req, &error_message); + if (type < 0) + return type; + + r = ensure_sane_request(server, req, message); + if (r < 0) + return r; + + request_set_timestamp(req, timestamp); + + r = dhcp_server_cleanup_expired_leases(server); + if (r < 0) + return r; + + existing_lease = hashmap_get(server->bound_leases_by_client_id, &req->client_id); + r = server_get_static_lease(server, req, &static_lease); + if (r < 0) + return r; + + switch (type) { + + case DHCP_DISCOVER: { + be32_t address = INADDR_ANY; + + log_dhcp_server(server, "DISCOVER (0x%x)", be32toh(req->message->xid)); + + if (server->pool_size == 0) + /* no pool allocated */ + return 0; + + /* for now pick a random free address from the pool */ + if (static_lease) + address = static_lease->address; + else if (existing_lease) + address = existing_lease->address; + else { + struct siphash state; + uint64_t hash; + + /* even with no persistence of leases, we try to offer the same client + the same IP address. we do this by using the hash of the client id + as the offset into the pool of leases when finding the next free one */ + + siphash24_init(&state, HASH_KEY.bytes); + client_id_hash_func(&req->client_id, &state); + hash = htole64(siphash24_finalize(&state)); + + for (unsigned i = 0; i < server->pool_size; i++) { + be32_t tmp_address; + + tmp_address = server->subnet | htobe32(server->pool_offset + (hash + i) % server->pool_size); + if (address_available(server, tmp_address)) { + address = tmp_address; + break; + } + } + } + + if (address == INADDR_ANY) + /* no free addresses left */ + return 0; + + if (server->rapid_commit && req->rapid_commit) + return server_ack_request(server, req, existing_lease, address); + + r = server_send_offer_or_ack(server, req, address, DHCP_OFFER); + if (r < 0) + /* this only fails on critical errors */ + return log_dhcp_server_errno(server, r, "Could not send offer: %m"); + + log_dhcp_server(server, "OFFER (0x%x)", be32toh(req->message->xid)); + return DHCP_OFFER; + } + case DHCP_DECLINE: + log_dhcp_server(server, "DECLINE (0x%x): %s", be32toh(req->message->xid), strna(error_message)); + + /* TODO: make sure we don't offer this address again */ + + return 1; + + case DHCP_REQUEST: { + be32_t address; + bool init_reboot = false; + + /* see RFC 2131, section 4.3.2 */ + + if (req->server_id != 0) { + log_dhcp_server(server, "REQUEST (selecting) (0x%x)", + be32toh(req->message->xid)); + + /* SELECTING */ + if (req->server_id != server->address) + /* client did not pick us */ + return 0; + + if (req->message->ciaddr != 0) + /* this MUST be zero */ + return 0; + + if (req->requested_ip == 0) + /* this must be filled in with the yiaddr + from the chosen OFFER */ + return 0; + + address = req->requested_ip; + } else if (req->requested_ip != 0) { + log_dhcp_server(server, "REQUEST (init-reboot) (0x%x)", + be32toh(req->message->xid)); + + /* INIT-REBOOT */ + if (req->message->ciaddr != 0) + /* this MUST be zero */ + return 0; + + /* TODO: check more carefully if IP is correct */ + address = req->requested_ip; + init_reboot = true; + } else { + log_dhcp_server(server, "REQUEST (rebinding/renewing) (0x%x)", + be32toh(req->message->xid)); + + /* REBINDING / RENEWING */ + if (req->message->ciaddr == 0) + /* this MUST be filled in with clients IP address */ + return 0; + + address = req->message->ciaddr; + } + + /* Silently ignore Rapid Commit option in REQUEST message. */ + req->rapid_commit = false; + + /* disallow our own address */ + if (address == server->address) + return 0; + + if (static_lease) { + /* Found a static lease for the client ID. */ + + if (static_lease->address != address) + /* The client requested an address which is different from the static lease. Refuse. */ + return server_send_nak_or_ignore(server, init_reboot, req); + + return server_ack_request(server, req, existing_lease, address); + } + + if (address_is_in_pool(server, address)) { + /* The requested address is in the pool. */ + + if (existing_lease && existing_lease->address != address) + /* We previously assigned an address, but the client requested another one. Refuse. */ + return server_send_nak_or_ignore(server, init_reboot, req); + + return server_ack_request(server, req, existing_lease, address); + } + + return server_send_nak_or_ignore(server, init_reboot, req); + } + + case DHCP_RELEASE: { + log_dhcp_server(server, "RELEASE (0x%x)", + be32toh(req->message->xid)); + + if (!existing_lease) + return 0; + + if (existing_lease->address != req->message->ciaddr) + return 0; + + dhcp_lease_free(existing_lease); + + if (server->callback) + server->callback(server, SD_DHCP_SERVER_EVENT_LEASE_CHANGED, server->callback_userdata); + + return 0; + }} + + return 0; +} + +static size_t relay_agent_information_length(const char* agent_circuit_id, const char* agent_remote_id) { + size_t sum = 0; + if (agent_circuit_id) + sum += 2 + strlen(agent_circuit_id); + if (agent_remote_id) + sum += 2 + strlen(agent_remote_id); + return sum; +} + +static int server_receive_message(sd_event_source *s, int fd, + uint32_t revents, void *userdata) { + _cleanup_free_ DHCPMessage *message = NULL; + /* This needs to be initialized with zero. See #20741. */ + CMSG_BUFFER_TYPE(CMSG_SPACE_TIMEVAL + + CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; + sd_dhcp_server *server = ASSERT_PTR(userdata); + struct iovec iov = {}; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + ssize_t datagram_size, len; + int r; + + datagram_size = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(datagram_size) || ERRNO_IS_NEG_DISCONNECT(datagram_size)) + return 0; + if (datagram_size < 0) { + log_dhcp_server_errno(server, datagram_size, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + size_t buflen = datagram_size; + if (sd_dhcp_server_is_in_relay_mode(server)) + /* Preallocate the additional size for DHCP Relay Agent Information Option if needed */ + buflen += relay_agent_information_length(server->agent_circuit_id, server->agent_remote_id) + 2; + + message = malloc(buflen); + if (!message) + return -ENOMEM; + + iov = IOVEC_MAKE(message, datagram_size); + + len = recvmsg_safe(fd, &msg, 0); + if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len)) + return 0; + if (len < 0) { + log_dhcp_server_errno(server, len, "Could not receive message, ignoring: %m"); + return 0; + } + + if ((size_t) len < sizeof(DHCPMessage)) + return 0; + + /* TODO figure out if this can be done as a filter on the socket, like for IPv6 */ + struct in_pktinfo *info = CMSG_FIND_DATA(&msg, IPPROTO_IP, IP_PKTINFO, struct in_pktinfo); + if (info && info->ipi_ifindex != server->ifindex) + return 0; + + if (sd_dhcp_server_is_in_relay_mode(server)) { + r = dhcp_server_relay_message(server, message, len - sizeof(DHCPMessage), buflen); + if (r < 0) + log_dhcp_server_errno(server, r, "Couldn't relay message, ignoring: %m"); + } else { + r = dhcp_server_handle_message(server, message, (size_t) len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg)); + if (r < 0) + log_dhcp_server_errno(server, r, "Couldn't process incoming message, ignoring: %m"); + } + return 0; +} + +static void dhcp_server_update_lease_servers(sd_dhcp_server *server) { + assert(server); + assert(server->address != 0); + + /* Convert null address -> server address */ + + for (sd_dhcp_lease_server_type_t k = 0; k < _SD_DHCP_LEASE_SERVER_TYPE_MAX; k++) + for (size_t i = 0; i < server->servers[k].size; i++) + if (in4_addr_is_null(&server->servers[k].addr[i])) + server->servers[k].addr[i].s_addr = server->address; +} + +int sd_dhcp_server_start(sd_dhcp_server *server) { + int r; + + assert_return(server, -EINVAL); + assert_return(server->event, -EINVAL); + + if (sd_dhcp_server_is_running(server)) + return 0; + + assert_return(!server->receive_message, -EBUSY); + assert_return(server->fd_raw < 0, -EBUSY); + assert_return(server->fd < 0, -EBUSY); + assert_return(server->address != htobe32(INADDR_ANY), -EUNATCH); + + dhcp_server_update_lease_servers(server); + + r = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (r < 0) { + r = -errno; + goto on_error; + } + server->fd_raw = r; + + if (server->bind_to_interface) + r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_ANY, DHCP_PORT_SERVER, -1); + else + r = dhcp_network_bind_udp_socket(0, server->address, DHCP_PORT_SERVER, -1); + if (r < 0) + goto on_error; + server->fd = r; + + r = sd_event_add_io(server->event, &server->receive_message, + server->fd, EPOLLIN, + server_receive_message, server); + if (r < 0) + goto on_error; + + r = sd_event_source_set_priority(server->receive_message, + server->event_priority); + if (r < 0) + goto on_error; + + if (!server->bind_to_interface) { + r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_BROADCAST, DHCP_PORT_SERVER, -1); + if (r < 0) + goto on_error; + + server->fd_broadcast = r; + + r = sd_event_add_io(server->event, &server->receive_broadcast, + server->fd_broadcast, EPOLLIN, + server_receive_message, server); + if (r < 0) + goto on_error; + + r = sd_event_source_set_priority(server->receive_broadcast, + server->event_priority); + if (r < 0) + goto on_error; + } + + log_dhcp_server(server, "STARTED"); + + return 0; + +on_error: + sd_dhcp_server_stop(server); + return r; +} + +int sd_dhcp_server_forcerenew(sd_dhcp_server *server) { + DHCPLease *lease; + int r = 0; + + assert_return(server, -EINVAL); + + log_dhcp_server(server, "FORCERENEW"); + + HASHMAP_FOREACH(lease, server->bound_leases_by_client_id) + RET_GATHER(r, + server_send_forcerenew(server, lease->address, lease->gateway, + lease->htype, lease->hlen, lease->chaddr)); + return r; +} + +int sd_dhcp_server_set_bind_to_interface(sd_dhcp_server *server, int enabled) { + assert_return(server, -EINVAL); + assert_return(!sd_dhcp_server_is_running(server), -EBUSY); + + if (!!enabled == server->bind_to_interface) + return 0; + + server->bind_to_interface = enabled; + + return 1; +} + +int sd_dhcp_server_set_timezone(sd_dhcp_server *server, const char *tz) { + int r; + + assert_return(server, -EINVAL); + assert_return(timezone_is_valid(tz, LOG_DEBUG), -EINVAL); + + if (streq_ptr(tz, server->timezone)) + return 0; + + r = free_and_strdup(&server->timezone, tz); + if (r < 0) + return r; + + return 1; +} + +int sd_dhcp_server_set_max_lease_time(sd_dhcp_server *server, uint64_t t) { + assert_return(server, -EINVAL); + + server->max_lease_time = t; + return 0; +} + +int sd_dhcp_server_set_default_lease_time(sd_dhcp_server *server, uint64_t t) { + assert_return(server, -EINVAL); + + server->default_lease_time = t; + return 0; +} + +int sd_dhcp_server_set_ipv6_only_preferred_usec(sd_dhcp_server *server, uint64_t t) { + assert_return(server, -EINVAL); + + /* When 0 is set, disables the IPv6 only mode. */ + + /* Refuse too short timespan unless test mode is enabled. */ + if (t > 0 && t < MIN_V6ONLY_WAIT_USEC && !network_test_mode_enabled()) + return -EINVAL; + + server->ipv6_only_preferred_usec = t; + return 0; +} + +int sd_dhcp_server_set_rapid_commit(sd_dhcp_server *server, int enabled) { + assert_return(server, -EINVAL); + + server->rapid_commit = enabled; + return 0; +} + +int sd_dhcp_server_set_servers( + sd_dhcp_server *server, + sd_dhcp_lease_server_type_t what, + const struct in_addr addresses[], + size_t n_addresses) { + + struct in_addr *c = NULL; + + assert_return(server, -EINVAL); + assert_return(!sd_dhcp_server_is_running(server), -EBUSY); + assert_return(addresses || n_addresses == 0, -EINVAL); + assert_return(what >= 0, -EINVAL); + assert_return(what < _SD_DHCP_LEASE_SERVER_TYPE_MAX, -EINVAL); + + if (server->servers[what].size == n_addresses && + memcmp(server->servers[what].addr, addresses, sizeof(struct in_addr) * n_addresses) == 0) + return 0; + + if (n_addresses > 0) { + c = newdup(struct in_addr, addresses, n_addresses); + if (!c) + return -ENOMEM; + } + + free_and_replace(server->servers[what].addr, c); + server->servers[what].size = n_addresses; + return 1; +} + +int sd_dhcp_server_set_dns(sd_dhcp_server *server, const struct in_addr dns[], size_t n) { + return sd_dhcp_server_set_servers(server, SD_DHCP_LEASE_DNS, dns, n); +} +int sd_dhcp_server_set_ntp(sd_dhcp_server *server, const struct in_addr ntp[], size_t n) { + return sd_dhcp_server_set_servers(server, SD_DHCP_LEASE_NTP, ntp, n); +} +int sd_dhcp_server_set_sip(sd_dhcp_server *server, const struct in_addr sip[], size_t n) { + return sd_dhcp_server_set_servers(server, SD_DHCP_LEASE_SIP, sip, n); +} +int sd_dhcp_server_set_pop3(sd_dhcp_server *server, const struct in_addr pop3[], size_t n) { + return sd_dhcp_server_set_servers(server, SD_DHCP_LEASE_POP3, pop3, n); +} +int sd_dhcp_server_set_smtp(sd_dhcp_server *server, const struct in_addr smtp[], size_t n) { + return sd_dhcp_server_set_servers(server, SD_DHCP_LEASE_SMTP, smtp, n); +} +int sd_dhcp_server_set_lpr(sd_dhcp_server *server, const struct in_addr lpr[], size_t n) { + return sd_dhcp_server_set_servers(server, SD_DHCP_LEASE_LPR, lpr, n); +} + +int sd_dhcp_server_set_router(sd_dhcp_server *server, const struct in_addr *router) { + assert_return(server, -EINVAL); + + /* router is NULL: router option will not be appended. + * router is null address (0.0.0.0): the server address will be used as the router address. + * otherwise: the specified address will be used as the router address. */ + + server->emit_router = router; + if (router) + server->router_address = *router; + + return 0; +} + +int sd_dhcp_server_add_option(sd_dhcp_server *server, sd_dhcp_option *v) { + int r; + + assert_return(server, -EINVAL); + assert_return(v, -EINVAL); + + r = ordered_set_ensure_put(&server->extra_options, &dhcp_option_hash_ops, v); + if (r < 0) + return r; + + sd_dhcp_option_ref(v); + return 0; +} + +int sd_dhcp_server_add_vendor_option(sd_dhcp_server *server, sd_dhcp_option *v) { + int r; + + assert_return(server, -EINVAL); + assert_return(v, -EINVAL); + + r = ordered_set_ensure_put(&server->vendor_options, &dhcp_option_hash_ops, v); + if (r < 0) + return r; + + sd_dhcp_option_ref(v); + + return 1; +} + +int sd_dhcp_server_set_callback(sd_dhcp_server *server, sd_dhcp_server_callback_t cb, void *userdata) { + assert_return(server, -EINVAL); + + server->callback = cb; + server->callback_userdata = userdata; + + return 0; +} + +int sd_dhcp_server_set_relay_target(sd_dhcp_server *server, const struct in_addr *address) { + assert_return(server, -EINVAL); + assert_return(!sd_dhcp_server_is_running(server), -EBUSY); + + if (memcmp(address, &server->relay_target, sizeof(struct in_addr)) == 0) + return 0; + + server->relay_target = *address; + return 1; +} + +int sd_dhcp_server_set_relay_agent_information( + sd_dhcp_server *server, + const char *agent_circuit_id, + const char *agent_remote_id) { + _cleanup_free_ char *circuit_id_dup = NULL, *remote_id_dup = NULL; + + assert_return(server, -EINVAL); + + if (relay_agent_information_length(agent_circuit_id, agent_remote_id) > UINT8_MAX) + return -ENOBUFS; + + if (agent_circuit_id) { + circuit_id_dup = strdup(agent_circuit_id); + if (!circuit_id_dup) + return -ENOMEM; + } + + if (agent_remote_id) { + remote_id_dup = strdup(agent_remote_id); + if (!remote_id_dup) + return -ENOMEM; + } + + free_and_replace(server->agent_circuit_id, circuit_id_dup); + free_and_replace(server->agent_remote_id, remote_id_dup); + return 0; +} + +int sd_dhcp_server_set_static_lease( + sd_dhcp_server *server, + const struct in_addr *address, + uint8_t *client_id, + size_t client_id_size) { + + _cleanup_(dhcp_lease_freep) DHCPLease *lease = NULL; + int r; + + assert_return(server, -EINVAL); + assert_return(client_id, -EINVAL); + assert_return(client_id_size > 0, -EINVAL); + assert_return(!sd_dhcp_server_is_running(server), -EBUSY); + + /* Static lease with an empty or omitted address is a valid entry, + * the server removes any static lease with the specified mac address. */ + if (!address || address->s_addr == 0) { + DHCPClientId c; + + c = (DHCPClientId) { + .length = client_id_size, + .data = client_id, + }; + + dhcp_lease_free(hashmap_get(server->static_leases_by_client_id, &c)); + return 0; + } + + lease = new(DHCPLease, 1); + if (!lease) + return -ENOMEM; + + *lease = (DHCPLease) { + .address = address->s_addr, + .client_id.length = client_id_size, + }; + lease->client_id.data = memdup(client_id, client_id_size); + if (!lease->client_id.data) + return -ENOMEM; + + lease->server = server; /* This must be set just before hashmap_put(). */ + + r = hashmap_ensure_put(&server->static_leases_by_client_id, &dhcp_lease_hash_ops, &lease->client_id, lease); + if (r < 0) + return r; + r = hashmap_ensure_put(&server->static_leases_by_address, NULL, UINT32_TO_PTR(lease->address), lease); + if (r < 0) + return r; + + TAKE_PTR(lease); + return 0; +} diff --git a/src/libsystemd-network/sd-dhcp6-client.c b/src/libsystemd-network/sd-dhcp6-client.c new file mode 100644 index 0000000..c20367d --- /dev/null +++ b/src/libsystemd-network/sd-dhcp6-client.c @@ -0,0 +1,1594 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014-2015 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <sys/ioctl.h> +#include <linux/if_arp.h> +#include <linux/if_infiniband.h> + +#include "sd-dhcp6-client.h" + +#include "alloc-util.h" +#include "device-util.h" +#include "dhcp-identifier.h" +#include "dhcp6-internal.h" +#include "dhcp6-lease-internal.h" +#include "dns-domain.h" +#include "event-util.h" +#include "fd-util.h" +#include "hexdecoct.h" +#include "hostname-util.h" +#include "in-addr-util.h" +#include "iovec-util.h" +#include "random-util.h" +#include "socket-util.h" +#include "sort-util.h" +#include "strv.h" +#include "web-util.h" + +#define DHCP6_CLIENT_DONT_DESTROY(client) \ + _cleanup_(sd_dhcp6_client_unrefp) _unused_ sd_dhcp6_client *_dont_destroy_##client = sd_dhcp6_client_ref(client) + +static int client_start_transaction(sd_dhcp6_client *client, DHCP6State state); + +int sd_dhcp6_client_set_callback( + sd_dhcp6_client *client, + sd_dhcp6_client_callback_t cb, + void *userdata) { + + assert_return(client, -EINVAL); + + client->callback = cb; + client->userdata = userdata; + + return 0; +} + +int dhcp6_client_set_state_callback( + sd_dhcp6_client *client, + sd_dhcp6_client_callback_t cb, + void *userdata) { + + assert_return(client, -EINVAL); + + client->state_callback = cb; + client->state_userdata = userdata; + + return 0; +} + +int sd_dhcp6_client_set_ifindex(sd_dhcp6_client *client, int ifindex) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + assert_return(ifindex > 0, -EINVAL); + + client->ifindex = ifindex; + return 0; +} + +int sd_dhcp6_client_set_ifname(sd_dhcp6_client *client, const char *ifname) { + assert_return(client, -EINVAL); + assert_return(ifname, -EINVAL); + + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) + return -EINVAL; + + return free_and_strdup(&client->ifname, ifname); +} + +int sd_dhcp6_client_get_ifname(sd_dhcp6_client *client, const char **ret) { + int r; + + assert_return(client, -EINVAL); + + r = get_ifname(client->ifindex, &client->ifname); + if (r < 0) + return r; + + if (ret) + *ret = client->ifname; + + return 0; +} + +int sd_dhcp6_client_set_local_address( + sd_dhcp6_client *client, + const struct in6_addr *local_address) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + assert_return(local_address, -EINVAL); + assert_return(in6_addr_is_link_local(local_address) > 0, -EINVAL); + + client->local_address = *local_address; + + return 0; +} + +int sd_dhcp6_client_set_mac( + sd_dhcp6_client *client, + const uint8_t *addr, + size_t addr_len, + uint16_t arp_type) { + + assert_return(client, -EINVAL); + assert_return(addr, -EINVAL); + assert_return(addr_len <= sizeof(client->hw_addr.bytes), -EINVAL); + + /* Unlike the other setters, it is OK to set a new MAC address while the client is running, + * as the MAC address is used only when setting DUID or IAID. */ + + if (arp_type == ARPHRD_ETHER) + assert_return(addr_len == ETH_ALEN, -EINVAL); + else if (arp_type == ARPHRD_INFINIBAND) + assert_return(addr_len == INFINIBAND_ALEN, -EINVAL); + else { + client->arp_type = ARPHRD_NONE; + client->hw_addr.length = 0; + return 0; + } + + client->arp_type = arp_type; + hw_addr_set(&client->hw_addr, addr, addr_len); + + return 0; +} + +int sd_dhcp6_client_set_prefix_delegation_hint( + sd_dhcp6_client *client, + uint8_t prefixlen, + const struct in6_addr *pd_prefix) { + + _cleanup_free_ DHCP6Address *prefix = NULL; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + if (!pd_prefix) { + /* clear previous assignments. */ + dhcp6_ia_clear_addresses(&client->ia_pd); + return 0; + } + + assert_return(prefixlen > 0 && prefixlen <= 128, -EINVAL); + + prefix = new(DHCP6Address, 1); + if (!prefix) + return -ENOMEM; + + *prefix = (DHCP6Address) { + .iapdprefix.address = *pd_prefix, + .iapdprefix.prefixlen = prefixlen, + }; + + LIST_PREPEND(addresses, client->ia_pd.addresses, TAKE_PTR(prefix)); + return 1; +} + +int sd_dhcp6_client_add_vendor_option(sd_dhcp6_client *client, sd_dhcp6_option *v) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + if (!v) { + /* Clear the previous assignments. */ + ordered_set_clear(client->vendor_options); + return 0; + } + + r = ordered_set_ensure_put(&client->vendor_options, &dhcp6_option_hash_ops, v); + if (r < 0) + return r; + + sd_dhcp6_option_ref(v); + + return 1; +} + +static int client_ensure_duid(sd_dhcp6_client *client) { + assert(client); + + if (client->duid_len != 0) + return 0; + + return dhcp_identifier_set_duid_en(&client->duid, &client->duid_len); +} + +/** + * Sets DUID. If duid is non-null, the DUID is set to duid_type + duid + * without further modification. Otherwise, if duid_type is supported, DUID + * is set based on that type. Otherwise, an error is returned. + */ +int sd_dhcp6_client_set_duid_llt(sd_dhcp6_client *client, uint64_t llt_time) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + r = dhcp_identifier_set_duid_llt(&client->hw_addr, client->arp_type, llt_time, &client->duid, &client->duid_len); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set DUID-LLT: %m"); + + return 0; +} + +int sd_dhcp6_client_set_duid_ll(sd_dhcp6_client *client) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + r = dhcp_identifier_set_duid_ll(&client->hw_addr, client->arp_type, &client->duid, &client->duid_len); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set DUID-LL: %m"); + + return 0; +} + +int sd_dhcp6_client_set_duid_en(sd_dhcp6_client *client) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + r = dhcp_identifier_set_duid_en(&client->duid, &client->duid_len); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set DUID-EN: %m"); + + return 0; +} + +int sd_dhcp6_client_set_duid_uuid(sd_dhcp6_client *client) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + r = dhcp_identifier_set_duid_uuid(&client->duid, &client->duid_len); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set DUID-UUID: %m"); + + return 0; +} + +int sd_dhcp6_client_set_duid_raw(sd_dhcp6_client *client, uint16_t duid_type, const uint8_t *duid, size_t duid_len) { + int r; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + assert_return(duid || duid_len == 0, -EINVAL); + + r = dhcp_identifier_set_duid_raw(duid_type, duid, duid_len, &client->duid, &client->duid_len); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set DUID: %m"); + + return 0; +} + +int sd_dhcp6_client_duid_as_string( + sd_dhcp6_client *client, + char **duid) { + _cleanup_free_ char *p = NULL, *s = NULL, *t = NULL; + const char *v; + int r; + + assert_return(client, -EINVAL); + assert_return(client->duid_len > offsetof(struct duid, raw.data), -ENODATA); + assert_return(duid, -EINVAL); + + v = duid_type_to_string(be16toh(client->duid.type)); + if (v) { + s = strdup(v); + if (!s) + return -ENOMEM; + } else { + r = asprintf(&s, "%0x", client->duid.type); + if (r < 0) + return -ENOMEM; + } + + t = hexmem(client->duid.raw.data, client->duid_len - offsetof(struct duid, raw.data)); + if (!t) + return -ENOMEM; + + p = strjoin(s, ":", t); + if (!p) + return -ENOMEM; + + *duid = TAKE_PTR(p); + + return 0; +} + +int sd_dhcp6_client_set_iaid(sd_dhcp6_client *client, uint32_t iaid) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + client->ia_na.header.id = htobe32(iaid); + client->ia_pd.header.id = htobe32(iaid); + client->iaid_set = true; + + return 0; +} + +static int client_ensure_iaid(sd_dhcp6_client *client) { + int r; + uint32_t iaid; + + assert(client); + + if (client->iaid_set) + return 0; + + r = dhcp_identifier_set_iaid(client->dev, &client->hw_addr, + /* legacy_unstable_byteorder = */ true, + &iaid); + if (r < 0) + return r; + + client->ia_na.header.id = iaid; + client->ia_pd.header.id = iaid; + client->iaid_set = true; + + return 0; +} + +int sd_dhcp6_client_get_iaid(sd_dhcp6_client *client, uint32_t *iaid) { + assert_return(client, -EINVAL); + assert_return(iaid, -EINVAL); + + if (!client->iaid_set) + return -ENODATA; + + *iaid = be32toh(client->ia_na.header.id); + + return 0; +} + +int sd_dhcp6_client_set_fqdn( + sd_dhcp6_client *client, + const char *fqdn) { + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + /* Make sure FQDN qualifies as DNS and as Linux hostname */ + if (fqdn && + !(hostname_is_valid(fqdn, 0) && dns_name_is_valid(fqdn) > 0)) + return -EINVAL; + + return free_and_strdup(&client->fqdn, fqdn); +} + +int sd_dhcp6_client_set_information_request(sd_dhcp6_client *client, int enabled) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + client->information_request = enabled; + + return 0; +} + +int sd_dhcp6_client_get_information_request(sd_dhcp6_client *client, int *enabled) { + assert_return(client, -EINVAL); + assert_return(enabled, -EINVAL); + + *enabled = client->information_request; + + return 0; +} + +static int be16_compare_func(const be16_t *a, const be16_t *b) { + return CMP(be16toh(*a), be16toh(*b)); +} + +int sd_dhcp6_client_set_request_option(sd_dhcp6_client *client, uint16_t option) { + be16_t opt; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + if (!dhcp6_option_can_request(option)) + return -EINVAL; + + opt = htobe16(option); + if (typesafe_bsearch(&opt, client->req_opts, client->n_req_opts, be16_compare_func)) + return -EEXIST; + + if (!GREEDY_REALLOC(client->req_opts, client->n_req_opts + 1)) + return -ENOMEM; + + client->req_opts[client->n_req_opts++] = opt; + + /* Sort immediately to make the above binary search will work for the next time. */ + typesafe_qsort(client->req_opts, client->n_req_opts, be16_compare_func); + return 0; +} + +int sd_dhcp6_client_set_request_mud_url(sd_dhcp6_client *client, const char *mudurl) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + assert_return(mudurl, -EINVAL); + assert_return(strlen(mudurl) <= UINT8_MAX, -EINVAL); + assert_return(http_url_is_valid(mudurl), -EINVAL); + + return free_and_strdup(&client->mudurl, mudurl); +} + +int sd_dhcp6_client_set_request_user_class(sd_dhcp6_client *client, char * const *user_class) { + char **s; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + assert_return(!strv_isempty(user_class), -EINVAL); + + STRV_FOREACH(p, user_class) { + size_t len = strlen(*p); + + if (len > UINT16_MAX || len == 0) + return -EINVAL; + } + + s = strv_copy(user_class); + if (!s) + return -ENOMEM; + + return strv_free_and_replace(client->user_class, s); +} + +int sd_dhcp6_client_set_request_vendor_class(sd_dhcp6_client *client, char * const *vendor_class) { + char **s; + + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + assert_return(!strv_isempty(vendor_class), -EINVAL); + + STRV_FOREACH(p, vendor_class) { + size_t len = strlen(*p); + + if (len > UINT16_MAX || len == 0) + return -EINVAL; + } + + s = strv_copy(vendor_class); + if (!s) + return -ENOMEM; + + return strv_free_and_replace(client->vendor_class, s); +} + +int sd_dhcp6_client_get_prefix_delegation(sd_dhcp6_client *client, int *delegation) { + assert_return(client, -EINVAL); + assert_return(delegation, -EINVAL); + + *delegation = FLAGS_SET(client->request_ia, DHCP6_REQUEST_IA_PD); + + return 0; +} + +int sd_dhcp6_client_set_prefix_delegation(sd_dhcp6_client *client, int delegation) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + SET_FLAG(client->request_ia, DHCP6_REQUEST_IA_PD, delegation); + + return 0; +} + +int sd_dhcp6_client_get_address_request(sd_dhcp6_client *client, int *request) { + assert_return(client, -EINVAL); + assert_return(request, -EINVAL); + + *request = FLAGS_SET(client->request_ia, DHCP6_REQUEST_IA_NA); + + return 0; +} + +int sd_dhcp6_client_set_address_request(sd_dhcp6_client *client, int request) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + SET_FLAG(client->request_ia, DHCP6_REQUEST_IA_NA, request); + + return 0; +} + +int dhcp6_client_set_transaction_id(sd_dhcp6_client *client, uint32_t transaction_id) { + assert(client); + assert_se(network_test_mode_enabled()); + + /* This is for tests or fuzzers. */ + + client->transaction_id = transaction_id & htobe32(0x00ffffff); + + return 0; +} + +int sd_dhcp6_client_set_rapid_commit(sd_dhcp6_client *client, int enable) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + client->rapid_commit = enable; + return 0; +} + +int sd_dhcp6_client_set_send_release(sd_dhcp6_client *client, int enable) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + client->send_release = enable; + return 0; +} + +int sd_dhcp6_client_get_lease(sd_dhcp6_client *client, sd_dhcp6_lease **ret) { + assert_return(client, -EINVAL); + + if (!client->lease) + return -ENOMSG; + + if (ret) + *ret = client->lease; + + return 0; +} + +int sd_dhcp6_client_add_option(sd_dhcp6_client *client, sd_dhcp6_option *v) { + int r; + + assert_return(client, -EINVAL); + assert_return(v, -EINVAL); + + r = ordered_hashmap_ensure_put(&client->extra_options, &dhcp6_option_hash_ops, UINT_TO_PTR(v->option), v); + if (r < 0) + return r; + + sd_dhcp6_option_ref(v); + return 0; +} + +static void client_set_state(sd_dhcp6_client *client, DHCP6State state) { + assert(client); + + if (client->state == state) + return; + + log_dhcp6_client(client, "State changed: %s -> %s", + dhcp6_state_to_string(client->state), dhcp6_state_to_string(state)); + + client->state = state; + + if (client->state_callback) + client->state_callback(client, state, client->state_userdata); +} + +int dhcp6_client_get_state(sd_dhcp6_client *client) { + assert_return(client, -EINVAL); + + return client->state; +} + +static void client_notify(sd_dhcp6_client *client, int event) { + assert(client); + + if (client->callback) + client->callback(client, event, client->userdata); +} + +static void client_cleanup(sd_dhcp6_client *client) { + assert(client); + + client->lease = sd_dhcp6_lease_unref(client->lease); + + /* Reset IRT here. Otherwise, we cannot restart the client in the information requesting mode, + * even though the lease is freed below. */ + client->information_request_time_usec = 0; + client->information_refresh_time_usec = 0; + + (void) event_source_disable(client->receive_message); + (void) event_source_disable(client->timeout_resend); + (void) event_source_disable(client->timeout_expire); + (void) event_source_disable(client->timeout_t1); + (void) event_source_disable(client->timeout_t2); + + client_set_state(client, DHCP6_STATE_STOPPED); +} + +static void client_stop(sd_dhcp6_client *client, int error) { + DHCP6_CLIENT_DONT_DESTROY(client); + + assert(client); + + client_notify(client, error); + + client_cleanup(client); +} + +static int client_append_common_options_in_managed_mode( + sd_dhcp6_client *client, + uint8_t **buf, + size_t *offset, + const DHCP6IA *ia_na, + const DHCP6IA *ia_pd) { + + int r; + + assert(client); + assert(IN_SET(client->state, + DHCP6_STATE_SOLICITATION, + DHCP6_STATE_REQUEST, + DHCP6_STATE_RENEW, + DHCP6_STATE_REBIND, + DHCP6_STATE_STOPPING)); + assert(buf); + assert(*buf); + assert(offset); + + if (FLAGS_SET(client->request_ia, DHCP6_REQUEST_IA_NA) && ia_na) { + r = dhcp6_option_append_ia(buf, offset, ia_na); + if (r < 0) + return r; + } + + if (FLAGS_SET(client->request_ia, DHCP6_REQUEST_IA_PD) && ia_pd) { + r = dhcp6_option_append_ia(buf, offset, ia_pd); + if (r < 0) + return r; + } + + if (client->state != DHCP6_STATE_STOPPING) { + r = dhcp6_option_append_fqdn(buf, offset, client->fqdn); + if (r < 0) + return r; + } + + r = dhcp6_option_append_user_class(buf, offset, client->user_class); + if (r < 0) + return r; + + r = dhcp6_option_append_vendor_class(buf, offset, client->vendor_class); + if (r < 0) + return r; + + r = dhcp6_option_append_vendor_option(buf, offset, client->vendor_options); + if (r < 0) + return r; + + return 0; +} + +static DHCP6MessageType client_message_type_from_state(sd_dhcp6_client *client) { + assert(client); + + switch (client->state) { + case DHCP6_STATE_INFORMATION_REQUEST: + return DHCP6_MESSAGE_INFORMATION_REQUEST; + case DHCP6_STATE_SOLICITATION: + return DHCP6_MESSAGE_SOLICIT; + case DHCP6_STATE_REQUEST: + return DHCP6_MESSAGE_REQUEST; + case DHCP6_STATE_RENEW: + return DHCP6_MESSAGE_RENEW; + case DHCP6_STATE_REBIND: + return DHCP6_MESSAGE_REBIND; + case DHCP6_STATE_STOPPING: + return DHCP6_MESSAGE_RELEASE; + default: + assert_not_reached(); + } +} + +static int client_append_oro(sd_dhcp6_client *client, uint8_t **buf, size_t *offset) { + _cleanup_free_ be16_t *p = NULL; + be16_t *req_opts; + size_t n; + + assert(client); + assert(buf); + assert(*buf); + assert(offset); + + switch (client->state) { + case DHCP6_STATE_INFORMATION_REQUEST: + n = client->n_req_opts; + p = new(be16_t, n + 2); + if (!p) + return -ENOMEM; + + memcpy_safe(p, client->req_opts, n * sizeof(be16_t)); + p[n++] = htobe16(SD_DHCP6_OPTION_INFORMATION_REFRESH_TIME); /* RFC 8415 section 21.23 */ + p[n++] = htobe16(SD_DHCP6_OPTION_INF_MAX_RT); /* RFC 8415 section 21.25 */ + + typesafe_qsort(p, n, be16_compare_func); + req_opts = p; + break; + + case DHCP6_STATE_SOLICITATION: + n = client->n_req_opts; + p = new(be16_t, n + 1); + if (!p) + return -ENOMEM; + + memcpy_safe(p, client->req_opts, n * sizeof(be16_t)); + p[n++] = htobe16(SD_DHCP6_OPTION_SOL_MAX_RT); /* RFC 8415 section 21.24 */ + + typesafe_qsort(p, n, be16_compare_func); + req_opts = p; + break; + + case DHCP6_STATE_STOPPING: + return 0; + + default: + n = client->n_req_opts; + req_opts = client->req_opts; + } + + if (n == 0) + return 0; + + return dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_ORO, n * sizeof(be16_t), req_opts); +} + +static int client_append_mudurl(sd_dhcp6_client *client, uint8_t **buf, size_t *offset) { + assert(client); + assert(buf); + assert(*buf); + assert(offset); + + if (!client->mudurl) + return 0; + + if (client->state == DHCP6_STATE_STOPPING) + return 0; + + return dhcp6_option_append(buf, offset, SD_DHCP6_OPTION_MUD_URL_V6, + strlen(client->mudurl), client->mudurl); +} + +int dhcp6_client_send_message(sd_dhcp6_client *client) { + _cleanup_free_ uint8_t *buf = NULL; + struct in6_addr all_servers = + IN6ADDR_ALL_DHCP6_RELAY_AGENTS_AND_SERVERS_INIT; + struct sd_dhcp6_option *j; + usec_t elapsed_usec, time_now; + be16_t elapsed_time; + DHCP6Message *message; + size_t offset; + int r; + + assert(client); + assert(client->event); + + r = sd_event_now(client->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + return r; + + if (!GREEDY_REALLOC0(buf, offsetof(DHCP6Message, options))) + return -ENOMEM; + + message = (DHCP6Message*) buf; + message->transaction_id = client->transaction_id; + message->type = client_message_type_from_state(client); + offset = offsetof(DHCP6Message, options); + + switch (client->state) { + case DHCP6_STATE_INFORMATION_REQUEST: + break; + + case DHCP6_STATE_SOLICITATION: + if (client->rapid_commit) { + r = dhcp6_option_append(&buf, &offset, SD_DHCP6_OPTION_RAPID_COMMIT, 0, NULL); + if (r < 0) + return r; + } + + r = client_append_common_options_in_managed_mode(client, &buf, &offset, + &client->ia_na, &client->ia_pd); + if (r < 0) + return r; + break; + + case DHCP6_STATE_REQUEST: + case DHCP6_STATE_RENEW: + case DHCP6_STATE_STOPPING: + r = dhcp6_option_append(&buf, &offset, SD_DHCP6_OPTION_SERVERID, + client->lease->serverid_len, + client->lease->serverid); + if (r < 0) + return r; + + _fallthrough_; + case DHCP6_STATE_REBIND: + + assert(client->lease); + + r = client_append_common_options_in_managed_mode(client, &buf, &offset, + client->lease->ia_na, client->lease->ia_pd); + if (r < 0) + return r; + break; + + case DHCP6_STATE_BOUND: + case DHCP6_STATE_STOPPED: + default: + assert_not_reached(); + } + + r = client_append_mudurl(client, &buf, &offset); + if (r < 0) + return r; + + r = client_append_oro(client, &buf, &offset); + if (r < 0) + return r; + + assert(client->duid_len > 0); + r = dhcp6_option_append(&buf, &offset, SD_DHCP6_OPTION_CLIENTID, + client->duid_len, &client->duid); + if (r < 0) + return r; + + ORDERED_HASHMAP_FOREACH(j, client->extra_options) { + r = dhcp6_option_append(&buf, &offset, j->option, j->length, j->data); + if (r < 0) + return r; + } + + /* RFC 8415 Section 21.9. + * A client MUST include an Elapsed Time option in messages to indicate how long the client has + * been trying to complete a DHCP message exchange. */ + elapsed_usec = MIN(usec_sub_unsigned(time_now, client->transaction_start) / USEC_PER_MSEC / 10, (usec_t) UINT16_MAX); + elapsed_time = htobe16(elapsed_usec); + r = dhcp6_option_append(&buf, &offset, SD_DHCP6_OPTION_ELAPSED_TIME, sizeof(elapsed_time), &elapsed_time); + if (r < 0) + return r; + + r = dhcp6_network_send_udp_socket(client->fd, &all_servers, buf, offset); + if (r < 0) + return r; + + log_dhcp6_client(client, "Sent %s", + dhcp6_message_type_to_string(client_message_type_from_state(client))); + return 0; +} + +static usec_t client_timeout_compute_random(usec_t val) { + return usec_sub_unsigned(val, random_u64_range(val / 10)); +} + +static int client_timeout_resend(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp6_client *client = ASSERT_PTR(userdata); + usec_t init_retransmit_time, max_retransmit_time; + int r; + + assert(client->event); + + switch (client->state) { + case DHCP6_STATE_INFORMATION_REQUEST: + init_retransmit_time = DHCP6_INF_TIMEOUT; + max_retransmit_time = DHCP6_INF_MAX_RT; + break; + + case DHCP6_STATE_SOLICITATION: + + if (client->retransmit_count > 0 && client->lease) { + (void) client_start_transaction(client, DHCP6_STATE_REQUEST); + return 0; + } + + init_retransmit_time = DHCP6_SOL_TIMEOUT; + max_retransmit_time = DHCP6_SOL_MAX_RT; + break; + + case DHCP6_STATE_REQUEST: + + if (client->retransmit_count >= DHCP6_REQ_MAX_RC) { + client_stop(client, SD_DHCP6_CLIENT_EVENT_RETRANS_MAX); + return 0; + } + + init_retransmit_time = DHCP6_REQ_TIMEOUT; + max_retransmit_time = DHCP6_REQ_MAX_RT; + break; + + case DHCP6_STATE_RENEW: + init_retransmit_time = DHCP6_REN_TIMEOUT; + max_retransmit_time = DHCP6_REN_MAX_RT; + + /* RFC 3315, section 18.1.3. says max retransmit duration will + be the remaining time until T2. Instead of setting MRD, + wait for T2 to trigger with the same end result */ + break; + + case DHCP6_STATE_REBIND: + init_retransmit_time = DHCP6_REB_TIMEOUT; + max_retransmit_time = DHCP6_REB_MAX_RT; + + /* Also, instead of setting MRD, the expire timer is already set in client_enter_bound_state(). */ + break; + + case DHCP6_STATE_STOPPED: + case DHCP6_STATE_STOPPING: + case DHCP6_STATE_BOUND: + default: + assert_not_reached(); + } + + r = dhcp6_client_send_message(client); + if (r >= 0) + client->retransmit_count++; + + if (client->retransmit_time == 0) { + client->retransmit_time = client_timeout_compute_random(init_retransmit_time); + + if (client->state == DHCP6_STATE_SOLICITATION) + client->retransmit_time += init_retransmit_time / 10; + + } else if (client->retransmit_time > max_retransmit_time / 2) + client->retransmit_time = client_timeout_compute_random(max_retransmit_time); + else + client->retransmit_time += client_timeout_compute_random(client->retransmit_time); + + log_dhcp6_client(client, "Next retransmission in %s", + FORMAT_TIMESPAN(client->retransmit_time, USEC_PER_SEC)); + + r = event_reset_time_relative(client->event, &client->timeout_resend, + CLOCK_BOOTTIME, + client->retransmit_time, 10 * USEC_PER_MSEC, + client_timeout_resend, client, + client->event_priority, "dhcp6-resend-timer", true); + if (r < 0) + client_stop(client, r); + + return 0; +} + +static int client_start_transaction(sd_dhcp6_client *client, DHCP6State state) { + int r; + + assert(client); + assert(client->event); + + switch (state) { + case DHCP6_STATE_INFORMATION_REQUEST: + case DHCP6_STATE_SOLICITATION: + assert(client->state == DHCP6_STATE_STOPPED); + break; + case DHCP6_STATE_REQUEST: + assert(client->state == DHCP6_STATE_SOLICITATION); + break; + case DHCP6_STATE_RENEW: + assert(client->state == DHCP6_STATE_BOUND); + break; + case DHCP6_STATE_REBIND: + assert(IN_SET(client->state, DHCP6_STATE_BOUND, DHCP6_STATE_RENEW)); + break; + case DHCP6_STATE_STOPPED: + case DHCP6_STATE_STOPPING: + case DHCP6_STATE_BOUND: + default: + assert_not_reached(); + } + + client_set_state(client, state); + + client->retransmit_time = 0; + client->retransmit_count = 0; + client->transaction_id = random_u32() & htobe32(0x00ffffff); + + r = sd_event_now(client->event, CLOCK_BOOTTIME, &client->transaction_start); + if (r < 0) + goto error; + + r = event_reset_time(client->event, &client->timeout_resend, + CLOCK_BOOTTIME, + 0, 0, + client_timeout_resend, client, + client->event_priority, "dhcp6-resend-timeout", true); + if (r < 0) + goto error; + + r = sd_event_source_set_enabled(client->receive_message, SD_EVENT_ON); + if (r < 0) + goto error; + + return 0; + +error: + client_stop(client, r); + return r; +} + +static int client_timeout_expire(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp6_client *client = ASSERT_PTR(userdata); + DHCP6_CLIENT_DONT_DESTROY(client); + DHCP6State state; + + (void) event_source_disable(client->timeout_expire); + (void) event_source_disable(client->timeout_t2); + (void) event_source_disable(client->timeout_t1); + + state = client->state; + + client_stop(client, SD_DHCP6_CLIENT_EVENT_RESEND_EXPIRE); + + /* RFC 3315, section 18.1.4., says that "...the client may choose to + use a Solicit message to locate a new DHCP server..." */ + if (state == DHCP6_STATE_REBIND) + (void) client_start_transaction(client, DHCP6_STATE_SOLICITATION); + + return 0; +} + +static int client_timeout_t2(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp6_client *client = ASSERT_PTR(userdata); + + (void) event_source_disable(client->timeout_t2); + (void) event_source_disable(client->timeout_t1); + + log_dhcp6_client(client, "Timeout T2"); + + (void) client_start_transaction(client, DHCP6_STATE_REBIND); + + return 0; +} + +static int client_timeout_t1(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp6_client *client = ASSERT_PTR(userdata); + + (void) event_source_disable(client->timeout_t1); + + log_dhcp6_client(client, "Timeout T1"); + + (void) client_start_transaction(client, DHCP6_STATE_RENEW); + + return 0; +} + +static int client_enter_bound_state(sd_dhcp6_client *client) { + usec_t lifetime_t1, lifetime_t2, lifetime_valid; + int r; + + assert(client); + assert(client->lease); + assert(IN_SET(client->state, + DHCP6_STATE_SOLICITATION, + DHCP6_STATE_REQUEST, + DHCP6_STATE_RENEW, + DHCP6_STATE_REBIND)); + + (void) event_source_disable(client->receive_message); + (void) event_source_disable(client->timeout_resend); + + r = sd_dhcp6_lease_get_t1(client->lease, &lifetime_t1); + if (r < 0) + goto error; + + r = sd_dhcp6_lease_get_t2(client->lease, &lifetime_t2); + if (r < 0) + goto error; + + r = sd_dhcp6_lease_get_valid_lifetime(client->lease, &lifetime_valid); + if (r < 0) + goto error; + + lifetime_t2 = client_timeout_compute_random(lifetime_t2); + lifetime_t1 = client_timeout_compute_random(MIN(lifetime_t1, lifetime_t2)); + + if (lifetime_t1 == USEC_INFINITY) { + log_dhcp6_client(client, "Infinite T1"); + event_source_disable(client->timeout_t1); + } else { + log_dhcp6_client(client, "T1 expires in %s", FORMAT_TIMESPAN(lifetime_t1, USEC_PER_SEC)); + r = event_reset_time_relative(client->event, &client->timeout_t1, + CLOCK_BOOTTIME, + lifetime_t1, 10 * USEC_PER_SEC, + client_timeout_t1, client, + client->event_priority, "dhcp6-t1-timeout", true); + if (r < 0) + goto error; + } + + if (lifetime_t2 == USEC_INFINITY) { + log_dhcp6_client(client, "Infinite T2"); + event_source_disable(client->timeout_t2); + } else { + log_dhcp6_client(client, "T2 expires in %s", FORMAT_TIMESPAN(lifetime_t2, USEC_PER_SEC)); + r = event_reset_time_relative(client->event, &client->timeout_t2, + CLOCK_BOOTTIME, + lifetime_t2, 10 * USEC_PER_SEC, + client_timeout_t2, client, + client->event_priority, "dhcp6-t2-timeout", true); + if (r < 0) + goto error; + } + + if (lifetime_valid == USEC_INFINITY) { + log_dhcp6_client(client, "Infinite valid lifetime"); + event_source_disable(client->timeout_expire); + } else { + log_dhcp6_client(client, "Valid lifetime expires in %s", FORMAT_TIMESPAN(lifetime_valid, USEC_PER_SEC)); + + r = event_reset_time_relative(client->event, &client->timeout_expire, + CLOCK_BOOTTIME, + lifetime_valid, USEC_PER_SEC, + client_timeout_expire, client, + client->event_priority, "dhcp6-lease-expire", true); + if (r < 0) + goto error; + } + + client_set_state(client, DHCP6_STATE_BOUND); + client_notify(client, SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE); + return 0; + +error: + client_stop(client, r); + return r; +} + +static int log_invalid_message_type(sd_dhcp6_client *client, const DHCP6Message *message) { + const char *type_str; + + assert(client); + assert(message); + + type_str = dhcp6_message_type_to_string(message->type); + if (type_str) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received unexpected %s message, ignoring.", type_str); + else + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received unsupported message type %u, ignoring.", message->type); +} + +static int client_process_information( + sd_dhcp6_client *client, + DHCP6Message *message, + size_t len, + const triple_timestamp *timestamp, + const struct in6_addr *server_address) { + + _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease = NULL; + int r; + + assert(client); + assert(message); + + if (message->type != DHCP6_MESSAGE_REPLY) + return log_invalid_message_type(client, message); + + r = dhcp6_lease_new_from_message(client, message, len, timestamp, server_address, &lease); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to process received reply message, ignoring: %m"); + + log_dhcp6_client(client, "Processed %s message", dhcp6_message_type_to_string(message->type)); + + sd_dhcp6_lease_unref(client->lease); + client->lease = TAKE_PTR(lease); + + /* Do not call client_stop() here, as it frees the acquired lease. */ + (void) event_source_disable(client->receive_message); + (void) event_source_disable(client->timeout_resend); + client_set_state(client, DHCP6_STATE_STOPPED); + + client_notify(client, SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST); + return 0; +} + +static int client_process_reply( + sd_dhcp6_client *client, + DHCP6Message *message, + size_t len, + const triple_timestamp *timestamp, + const struct in6_addr *server_address) { + + _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease = NULL; + int r; + + assert(client); + assert(message); + + if (message->type != DHCP6_MESSAGE_REPLY) + return log_invalid_message_type(client, message); + + r = dhcp6_lease_new_from_message(client, message, len, timestamp, server_address, &lease); + if (r == -EADDRNOTAVAIL) { + + /* If NoBinding status code is received, we cannot request the address anymore. + * Let's restart transaction from the beginning. */ + + if (client->state == DHCP6_STATE_REQUEST) + /* The lease is not acquired yet, hence it is not necessary to notify the restart. */ + client_cleanup(client); + else + /* We need to notify the previous lease was expired. */ + client_stop(client, SD_DHCP6_CLIENT_EVENT_RESEND_EXPIRE); + + return client_start_transaction(client, DHCP6_STATE_SOLICITATION); + } + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to process received reply message, ignoring: %m"); + + log_dhcp6_client(client, "Processed %s message", dhcp6_message_type_to_string(message->type)); + + sd_dhcp6_lease_unref(client->lease); + client->lease = TAKE_PTR(lease); + + return client_enter_bound_state(client); +} + +static int client_process_advertise_or_rapid_commit_reply( + sd_dhcp6_client *client, + DHCP6Message *message, + size_t len, + const triple_timestamp *timestamp, + const struct in6_addr *server_address) { + + _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease = NULL; + uint8_t pref_advertise, pref_lease = 0; + int r; + + assert(client); + assert(message); + + if (!IN_SET(message->type, DHCP6_MESSAGE_ADVERTISE, DHCP6_MESSAGE_REPLY)) + return log_invalid_message_type(client, message); + + r = dhcp6_lease_new_from_message(client, message, len, timestamp, server_address, &lease); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to process received %s message, ignoring: %m", + dhcp6_message_type_to_string(message->type)); + + if (message->type == DHCP6_MESSAGE_REPLY) { + bool rapid_commit; + + if (!client->rapid_commit) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received unexpected reply message, even we sent a solicit message without the rapid commit option, ignoring."); + + r = dhcp6_lease_get_rapid_commit(lease, &rapid_commit); + if (r < 0) + return r; + + if (!rapid_commit) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received reply message without rapid commit flag, ignoring."); + + log_dhcp6_client(client, "Processed %s message", dhcp6_message_type_to_string(message->type)); + + sd_dhcp6_lease_unref(client->lease); + client->lease = TAKE_PTR(lease); + + return client_enter_bound_state(client); + } + + r = dhcp6_lease_get_preference(lease, &pref_advertise); + if (r < 0) + return r; + + if (client->lease) { + r = dhcp6_lease_get_preference(client->lease, &pref_lease); + if (r < 0) + return r; + } + + log_dhcp6_client(client, "Processed %s message", dhcp6_message_type_to_string(message->type)); + + if (!client->lease || pref_advertise > pref_lease) { + /* If this is the first advertise message or has higher preference, then save the lease. */ + sd_dhcp6_lease_unref(client->lease); + client->lease = TAKE_PTR(lease); + } + + if (pref_advertise == 255 || client->retransmit_count > 1) + (void) client_start_transaction(client, DHCP6_STATE_REQUEST); + + return 0; +} + +static int client_receive_message( + sd_event_source *s, + int fd, uint32_t + revents, + void *userdata) { + + sd_dhcp6_client *client = ASSERT_PTR(userdata); + DHCP6_CLIENT_DONT_DESTROY(client); + /* This needs to be initialized with zero. See #20741. */ + CMSG_BUFFER_TYPE(CMSG_SPACE_TIMEVAL) control = {}; + struct iovec iov; + union sockaddr_union sa = {}; + struct msghdr msg = { + .msg_name = &sa.sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + triple_timestamp t; + _cleanup_free_ DHCP6Message *message = NULL; + struct in6_addr *server_address = NULL; + ssize_t buflen, len; + + buflen = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(buflen) || ERRNO_IS_NEG_DISCONNECT(buflen)) + return 0; + if (buflen < 0) { + log_dhcp6_client_errno(client, buflen, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + message = malloc(buflen); + if (!message) + return -ENOMEM; + + iov = IOVEC_MAKE(message, buflen); + + len = recvmsg_safe(fd, &msg, MSG_DONTWAIT); + if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len)) + return 0; + if (len < 0) { + log_dhcp6_client_errno(client, len, "Could not receive message from UDP socket, ignoring: %m"); + return 0; + } + if ((size_t) len < sizeof(DHCP6Message)) { + log_dhcp6_client(client, "Too small to be DHCP6 message: ignoring"); + return 0; + } + + /* msg_namelen == 0 happens when running the test-suite over a socketpair */ + if (msg.msg_namelen > 0) { + if (msg.msg_namelen != sizeof(struct sockaddr_in6) || sa.in6.sin6_family != AF_INET6) { + log_dhcp6_client(client, "Received message from invalid source, ignoring."); + return 0; + } + + server_address = &sa.in6.sin6_addr; + } + + triple_timestamp_from_cmsg(&t, &msg); + + if (client->transaction_id != (message->transaction_id & htobe32(0x00ffffff))) + return 0; + + switch (client->state) { + case DHCP6_STATE_INFORMATION_REQUEST: + if (client_process_information(client, message, len, &t, server_address) < 0) + return 0; + break; + + case DHCP6_STATE_SOLICITATION: + if (client_process_advertise_or_rapid_commit_reply(client, message, len, &t, server_address) < 0) + return 0; + break; + + case DHCP6_STATE_REQUEST: + case DHCP6_STATE_RENEW: + case DHCP6_STATE_REBIND: + if (client_process_reply(client, message, len, &t, server_address) < 0) + return 0; + break; + + case DHCP6_STATE_BOUND: + case DHCP6_STATE_STOPPED: + case DHCP6_STATE_STOPPING: + default: + assert_not_reached(); + } + + return 0; +} + +static int client_send_release(sd_dhcp6_client *client) { + sd_dhcp6_lease *lease; + + assert(client); + + if (!client->send_release) + return 0; + + if (sd_dhcp6_client_get_lease(client, &lease) < 0) + return 0; + + if (!lease->ia_na && !lease->ia_pd) + return 0; + + client_set_state(client, DHCP6_STATE_STOPPING); + return dhcp6_client_send_message(client); +} + +int sd_dhcp6_client_stop(sd_dhcp6_client *client) { + int r; + + if (!client) + return 0; + + /* Intentionally ignoring failure to send DHCP6 release. The DHCPv6 client + * engine is about to release its UDP socket unconditionally. */ + r = client_send_release(client); + if (r < 0) + log_dhcp6_client_errno(client, r, + "Failed to send DHCP6 release message, ignoring: %m"); + + client_stop(client, SD_DHCP6_CLIENT_EVENT_STOP); + + client->receive_message = sd_event_source_unref(client->receive_message); + client->fd = safe_close(client->fd); + + return 0; +} + +int sd_dhcp6_client_is_running(sd_dhcp6_client *client) { + assert_return(client, -EINVAL); + + return client->state != DHCP6_STATE_STOPPED; +} + +int sd_dhcp6_client_start(sd_dhcp6_client *client) { + DHCP6State state = DHCP6_STATE_SOLICITATION; + int r; + + assert_return(client, -EINVAL); + assert_return(client->event, -EINVAL); + assert_return(client->ifindex > 0, -EINVAL); + assert_return(in6_addr_is_link_local(&client->local_address) > 0, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + assert_return(client->information_request || client->request_ia != 0, -EINVAL); + + /* Even if the client is in the STOPPED state, the lease acquired in the previous information + * request may be stored. */ + client->lease = sd_dhcp6_lease_unref(client->lease); + + r = client_ensure_iaid(client); + if (r < 0) + return r; + + r = client_ensure_duid(client); + if (r < 0) + return r; + + if (client->fd < 0) { + r = dhcp6_network_bind_udp_socket(client->ifindex, &client->local_address); + if (r < 0) + return log_dhcp6_client_errno(client, r, + "Failed to bind to UDP socket at address %s: %m", + IN6_ADDR_TO_STRING(&client->local_address)); + + client->fd = r; + } + + if (!client->receive_message) { + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *s = NULL; + + r = sd_event_add_io(client->event, &s, client->fd, EPOLLIN, client_receive_message, client); + if (r < 0) + return r; + + r = sd_event_source_set_priority(s, client->event_priority); + if (r < 0) + return r; + + r = sd_event_source_set_description(s, "dhcp6-receive-message"); + if (r < 0) + return r; + + client->receive_message = TAKE_PTR(s); + } + + if (client->information_request) { + usec_t t = now(CLOCK_MONOTONIC); + + if (t < usec_add(client->information_request_time_usec, client->information_refresh_time_usec)) + return 0; + + client->information_request_time_usec = t; + state = DHCP6_STATE_INFORMATION_REQUEST; + } + + log_dhcp6_client(client, "Starting in %s mode", + client->information_request ? "Information request" : "Solicit"); + + return client_start_transaction(client, state); +} + +int sd_dhcp6_client_attach_event(sd_dhcp6_client *client, sd_event *event, int64_t priority) { + int r; + + assert_return(client, -EINVAL); + assert_return(!client->event, -EBUSY); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + if (event) + client->event = sd_event_ref(event); + else { + r = sd_event_default(&client->event); + if (r < 0) + return 0; + } + + client->event_priority = priority; + + return 0; +} + +int sd_dhcp6_client_detach_event(sd_dhcp6_client *client) { + assert_return(client, -EINVAL); + assert_return(!sd_dhcp6_client_is_running(client), -EBUSY); + + client->event = sd_event_unref(client->event); + + return 0; +} + +sd_event *sd_dhcp6_client_get_event(sd_dhcp6_client *client) { + assert_return(client, NULL); + + return client->event; +} + +int sd_dhcp6_client_attach_device(sd_dhcp6_client *client, sd_device *dev) { + assert_return(client, -EINVAL); + + return device_unref_and_replace(client->dev, dev); +} + +static sd_dhcp6_client *dhcp6_client_free(sd_dhcp6_client *client) { + if (!client) + return NULL; + + sd_dhcp6_lease_unref(client->lease); + + sd_event_source_disable_unref(client->receive_message); + sd_event_source_disable_unref(client->timeout_resend); + sd_event_source_disable_unref(client->timeout_expire); + sd_event_source_disable_unref(client->timeout_t1); + sd_event_source_disable_unref(client->timeout_t2); + sd_event_unref(client->event); + + client->fd = safe_close(client->fd); + + sd_device_unref(client->dev); + + free(client->req_opts); + free(client->fqdn); + free(client->mudurl); + dhcp6_ia_clear_addresses(&client->ia_pd); + ordered_hashmap_free(client->extra_options); + ordered_set_free(client->vendor_options); + strv_free(client->user_class); + strv_free(client->vendor_class); + free(client->ifname); + + return mfree(client); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp6_client, sd_dhcp6_client, dhcp6_client_free); + +int sd_dhcp6_client_new(sd_dhcp6_client **ret) { + _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL; + + assert_return(ret, -EINVAL); + + client = new(sd_dhcp6_client, 1); + if (!client) + return -ENOMEM; + + *client = (sd_dhcp6_client) { + .n_ref = 1, + .ia_na.type = SD_DHCP6_OPTION_IA_NA, + .ia_pd.type = SD_DHCP6_OPTION_IA_PD, + .ifindex = -1, + .request_ia = DHCP6_REQUEST_IA_NA | DHCP6_REQUEST_IA_PD, + .fd = -EBADF, + .rapid_commit = true, + }; + + *ret = TAKE_PTR(client); + + return 0; +} diff --git a/src/libsystemd-network/sd-dhcp6-lease.c b/src/libsystemd-network/sd-dhcp6-lease.c new file mode 100644 index 0000000..674248b --- /dev/null +++ b/src/libsystemd-network/sd-dhcp6-lease.c @@ -0,0 +1,964 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014-2015 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> + +#include "alloc-util.h" +#include "dhcp6-internal.h" +#include "dhcp6-lease-internal.h" +#include "network-common.h" +#include "strv.h" + +#define IRT_DEFAULT (1 * USEC_PER_DAY) +#define IRT_MINIMUM (600 * USEC_PER_SEC) + +static void dhcp6_lease_set_timestamp(sd_dhcp6_lease *lease, const triple_timestamp *timestamp) { + assert(lease); + + if (timestamp && triple_timestamp_is_set(timestamp)) + lease->timestamp = *timestamp; + else + triple_timestamp_now(&lease->timestamp); +} + +int sd_dhcp6_lease_get_timestamp(sd_dhcp6_lease *lease, clockid_t clock, uint64_t *ret) { + assert_return(lease, -EINVAL); + assert_return(TRIPLE_TIMESTAMP_HAS_CLOCK(clock), -EOPNOTSUPP); + assert_return(clock_supported(clock), -EOPNOTSUPP); + assert_return(ret, -EINVAL); + + if (!triple_timestamp_is_set(&lease->timestamp)) + return -ENODATA; + + *ret = triple_timestamp_by_clock(&lease->timestamp, clock); + return 0; +} + +static void dhcp6_lease_set_lifetime(sd_dhcp6_lease *lease) { + usec_t t1 = USEC_INFINITY, t2 = USEC_INFINITY, min_valid_lt = USEC_INFINITY; + + assert(lease); + assert(lease->ia_na || lease->ia_pd); + + if (lease->ia_na) { + t1 = MIN(t1, be32_sec_to_usec(lease->ia_na->header.lifetime_t1, /* max_as_infinity = */ true)); + t2 = MIN(t2, be32_sec_to_usec(lease->ia_na->header.lifetime_t2, /* max_as_infinity = */ true)); + + LIST_FOREACH(addresses, a, lease->ia_na->addresses) + min_valid_lt = MIN(min_valid_lt, be32_sec_to_usec(a->iaaddr.lifetime_valid, /* max_as_infinity = */ true)); + } + + if (lease->ia_pd) { + t1 = MIN(t1, be32_sec_to_usec(lease->ia_pd->header.lifetime_t1, /* max_as_infinity = */ true)); + t2 = MIN(t2, be32_sec_to_usec(lease->ia_pd->header.lifetime_t2, /* max_as_infinity = */ true)); + + LIST_FOREACH(addresses, a, lease->ia_pd->addresses) + min_valid_lt = MIN(min_valid_lt, be32_sec_to_usec(a->iapdprefix.lifetime_valid, /* max_as_infinity = */ true)); + } + + if (t2 == 0 || t2 > min_valid_lt) { + /* If T2 is zero or longer than the minimum valid lifetime of the addresses or prefixes, + * then adjust lifetime with it. */ + t1 = min_valid_lt / 2; + t2 = min_valid_lt / 10 * 8; + } + + lease->lifetime_valid = min_valid_lt; + lease->lifetime_t1 = t1; + lease->lifetime_t2 = t2; +} + +#define DEFINE_GET_TIME_FUNCTIONS(name, val) \ + int sd_dhcp6_lease_get_##name( \ + sd_dhcp6_lease *lease, \ + uint64_t *ret) { \ + \ + assert_return(lease, -EINVAL); \ + \ + if (!lease->ia_na && !lease->ia_pd) \ + return -ENODATA; \ + \ + if (ret) \ + *ret = lease->val; \ + return 0; \ + } \ + \ + int sd_dhcp6_lease_get_##name##_timestamp( \ + sd_dhcp6_lease *lease, \ + clockid_t clock, \ + uint64_t *ret) { \ + \ + usec_t s, t; \ + int r; \ + \ + assert_return(lease, -EINVAL); \ + \ + r = sd_dhcp6_lease_get_##name(lease, &s); \ + if (r < 0) \ + return r; \ + \ + r = sd_dhcp6_lease_get_timestamp(lease, clock, &t); \ + if (r < 0) \ + return r; \ + \ + if (ret) \ + *ret = time_span_to_stamp(s, t); \ + return 0; \ + } + +DEFINE_GET_TIME_FUNCTIONS(t1, lifetime_t1); +DEFINE_GET_TIME_FUNCTIONS(t2, lifetime_t1); +DEFINE_GET_TIME_FUNCTIONS(valid_lifetime, lifetime_valid); + +static void dhcp6_lease_set_server_address(sd_dhcp6_lease *lease, const struct in6_addr *server_address) { + assert(lease); + + if (server_address) + lease->server_address = *server_address; + else + lease->server_address = (struct in6_addr) {}; +} + +int sd_dhcp6_lease_get_server_address(sd_dhcp6_lease *lease, struct in6_addr *ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + *ret = lease->server_address; + return 0; +} + +void dhcp6_ia_clear_addresses(DHCP6IA *ia) { + assert(ia); + + LIST_FOREACH(addresses, a, ia->addresses) + free(a); + + ia->addresses = NULL; +} + +DHCP6IA *dhcp6_ia_free(DHCP6IA *ia) { + if (!ia) + return NULL; + + dhcp6_ia_clear_addresses(ia); + + return mfree(ia); +} + +int dhcp6_lease_set_clientid(sd_dhcp6_lease *lease, const uint8_t *id, size_t len) { + uint8_t *clientid = NULL; + + assert(lease); + assert(id || len == 0); + + if (len > 0) { + clientid = memdup(id, len); + if (!clientid) + return -ENOMEM; + } + + free_and_replace(lease->clientid, clientid); + lease->clientid_len = len; + + return 0; +} + +int dhcp6_lease_get_clientid(sd_dhcp6_lease *lease, uint8_t **ret_id, size_t *ret_len) { + assert(lease); + + if (!lease->clientid) + return -ENODATA; + + if (ret_id) + *ret_id = lease->clientid; + if (ret_len) + *ret_len = lease->clientid_len; + + return 0; +} + +int dhcp6_lease_set_serverid(sd_dhcp6_lease *lease, const uint8_t *id, size_t len) { + uint8_t *serverid = NULL; + + assert(lease); + assert(id || len == 0); + + if (len > 0) { + serverid = memdup(id, len); + if (!serverid) + return -ENOMEM; + } + + free_and_replace(lease->serverid, serverid); + lease->serverid_len = len; + + return 0; +} + +int dhcp6_lease_get_serverid(sd_dhcp6_lease *lease, uint8_t **ret_id, size_t *ret_len) { + assert(lease); + + if (!lease->serverid) + return -ENODATA; + + if (ret_id) + *ret_id = lease->serverid; + if (ret_len) + *ret_len = lease->serverid_len; + return 0; +} + +int dhcp6_lease_set_preference(sd_dhcp6_lease *lease, uint8_t preference) { + assert(lease); + + lease->preference = preference; + return 0; +} + +int dhcp6_lease_get_preference(sd_dhcp6_lease *lease, uint8_t *ret) { + assert(lease); + assert(ret); + + *ret = lease->preference; + return 0; +} + +int dhcp6_lease_set_rapid_commit(sd_dhcp6_lease *lease) { + assert(lease); + + lease->rapid_commit = true; + return 0; +} + +int dhcp6_lease_get_rapid_commit(sd_dhcp6_lease *lease, bool *ret) { + assert(lease); + assert(ret); + + *ret = lease->rapid_commit; + return 0; +} + +int sd_dhcp6_lease_get_address(sd_dhcp6_lease *lease, struct in6_addr *ret) { + assert_return(lease, -EINVAL); + + if (!lease->addr_iter) + return -ENODATA; + + if (ret) + *ret = lease->addr_iter->iaaddr.address; + return 0; +} + +int sd_dhcp6_lease_get_address_lifetime( + sd_dhcp6_lease *lease, + usec_t *ret_lifetime_preferred, + usec_t *ret_lifetime_valid) { + + const struct iaaddr *a; + + assert_return(lease, -EINVAL); + + if (!lease->addr_iter) + return -ENODATA; + + a = &lease->addr_iter->iaaddr; + + if (ret_lifetime_preferred) + *ret_lifetime_preferred = be32_sec_to_usec(a->lifetime_preferred, /* max_as_infinity = */ true); + if (ret_lifetime_valid) + *ret_lifetime_valid = be32_sec_to_usec(a->lifetime_valid, /* max_as_infinity = */ true); + return 0; +} + +int sd_dhcp6_lease_address_iterator_reset(sd_dhcp6_lease *lease) { + if (!lease) + return false; + + lease->addr_iter = lease->ia_na ? lease->ia_na->addresses : NULL; + return !!lease->addr_iter; +} + +int sd_dhcp6_lease_address_iterator_next(sd_dhcp6_lease *lease) { + if (!lease || !lease->addr_iter) + return false; + + lease->addr_iter = lease->addr_iter->addresses_next; + return !!lease->addr_iter; +} + +int sd_dhcp6_lease_has_address(sd_dhcp6_lease *lease) { + return lease && lease->ia_na; +} + +int sd_dhcp6_lease_get_pd_prefix( + sd_dhcp6_lease *lease, + struct in6_addr *ret_prefix, + uint8_t *ret_prefix_len) { + + const struct iapdprefix *a; + + assert_return(lease, -EINVAL); + + if (!lease->prefix_iter) + return -ENODATA; + + a = &lease->prefix_iter->iapdprefix; + + if (ret_prefix) + *ret_prefix = a->address; + if (ret_prefix_len) + *ret_prefix_len = a->prefixlen; + return 0; +} + +int sd_dhcp6_lease_get_pd_lifetime( + sd_dhcp6_lease *lease, + uint64_t *ret_lifetime_preferred, + uint64_t *ret_lifetime_valid) { + + const struct iapdprefix *a; + + assert_return(lease, -EINVAL); + + if (!lease->prefix_iter) + return -ENODATA; + + a = &lease->prefix_iter->iapdprefix; + + if (ret_lifetime_preferred) + *ret_lifetime_preferred = be32_sec_to_usec(a->lifetime_preferred, /* max_as_infinity = */ true); + if (ret_lifetime_valid) + *ret_lifetime_valid = be32_sec_to_usec(a->lifetime_valid, /* max_as_infinity = */ true); + return 0; +} + +int sd_dhcp6_lease_pd_iterator_reset(sd_dhcp6_lease *lease) { + if (!lease) + return false; + + lease->prefix_iter = lease->ia_pd ? lease->ia_pd->addresses : NULL; + return !!lease->prefix_iter; +} + +int sd_dhcp6_lease_pd_iterator_next(sd_dhcp6_lease *lease) { + if (!lease || !lease->prefix_iter) + return false; + + lease->prefix_iter = lease->prefix_iter->addresses_next; + return !!lease->prefix_iter; +} + +#define DEFINE_GET_TIMESTAMP2(name) \ + int sd_dhcp6_lease_get_##name##_lifetime_timestamp( \ + sd_dhcp6_lease *lease, \ + clockid_t clock, \ + uint64_t *ret_lifetime_preferred, \ + uint64_t *ret_lifetime_valid) { \ + \ + usec_t t, p, v; \ + int r; \ + \ + assert_return(lease, -EINVAL); \ + \ + r = sd_dhcp6_lease_get_##name##_lifetime( \ + lease, \ + ret_lifetime_preferred ? &p : NULL, \ + ret_lifetime_valid ? &v : NULL); \ + if (r < 0) \ + return r; \ + \ + r = sd_dhcp6_lease_get_timestamp(lease, clock, &t); \ + if (r < 0) \ + return r; \ + \ + if (ret_lifetime_preferred) \ + *ret_lifetime_preferred = time_span_to_stamp(p, t); \ + if (ret_lifetime_valid) \ + *ret_lifetime_valid = time_span_to_stamp(v, t); \ + return 0; \ + } + +DEFINE_GET_TIMESTAMP2(address); +DEFINE_GET_TIMESTAMP2(pd); + +int sd_dhcp6_lease_has_pd_prefix(sd_dhcp6_lease *lease) { + return lease && lease->ia_pd; +} + +int dhcp6_lease_add_dns(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + assert(lease); + assert(optval || optlen == 0); + + if (optlen == 0) + return 0; + + return dhcp6_option_parse_addresses(optval, optlen, &lease->dns, &lease->dns_count); +} + +int sd_dhcp6_lease_get_dns(sd_dhcp6_lease *lease, const struct in6_addr **ret) { + assert_return(lease, -EINVAL); + + if (!lease->dns) + return -ENODATA; + + if (ret) + *ret = lease->dns; + + return lease->dns_count; +} + +int dhcp6_lease_add_domains(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + _cleanup_strv_free_ char **domains = NULL; + int r; + + assert(lease); + assert(optval || optlen == 0); + + if (optlen == 0) + return 0; + + r = dhcp6_option_parse_domainname_list(optval, optlen, &domains); + if (r < 0) + return r; + + return strv_extend_strv(&lease->domains, domains, true); +} + +int sd_dhcp6_lease_get_domains(sd_dhcp6_lease *lease, char ***ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (!lease->domains) + return -ENODATA; + + *ret = lease->domains; + return strv_length(lease->domains); +} + +int dhcp6_lease_add_ntp(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + int r; + + assert(lease); + assert(optval || optlen == 0); + + for (size_t offset = 0; offset < optlen;) { + const uint8_t *subval; + size_t sublen; + uint16_t subopt; + + r = dhcp6_option_parse(optval, optlen, &offset, &subopt, &sublen, &subval); + if (r < 0) + return r; + + switch (subopt) { + case DHCP6_NTP_SUBOPTION_SRV_ADDR: + case DHCP6_NTP_SUBOPTION_MC_ADDR: + if (sublen != 16) + return -EINVAL; + + r = dhcp6_option_parse_addresses(subval, sublen, &lease->ntp, &lease->ntp_count); + if (r < 0) + return r; + + break; + + case DHCP6_NTP_SUBOPTION_SRV_FQDN: { + _cleanup_free_ char *server = NULL; + + r = dhcp6_option_parse_domainname(subval, sublen, &server); + if (r < 0) + return r; + + if (strv_contains(lease->ntp_fqdn, server)) + continue; + + r = strv_consume(&lease->ntp_fqdn, TAKE_PTR(server)); + if (r < 0) + return r; + + break; + }} + } + + return 0; +} + +int dhcp6_lease_add_sntp(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + assert(lease); + assert(optval || optlen == 0); + + if (optlen == 0) + return 0; + + /* SNTP option is defined in RFC4075, and deprecated by RFC5908. */ + return dhcp6_option_parse_addresses(optval, optlen, &lease->sntp, &lease->sntp_count); +} + +int sd_dhcp6_lease_get_ntp_addrs(sd_dhcp6_lease *lease, const struct in6_addr **ret) { + assert_return(lease, -EINVAL); + + if (lease->ntp) { + if (ret) + *ret = lease->ntp; + return lease->ntp_count; + } + + if (lease->sntp && !lease->ntp_fqdn) { + /* Fallback to the deprecated SNTP option. */ + if (ret) + *ret = lease->sntp; + return lease->sntp_count; + } + + return -ENODATA; +} + +int sd_dhcp6_lease_get_ntp_fqdn(sd_dhcp6_lease *lease, char ***ret) { + assert_return(lease, -EINVAL); + + if (!lease->ntp_fqdn) + return -ENODATA; + + if (ret) + *ret = lease->ntp_fqdn; + return strv_length(lease->ntp_fqdn); +} + +int dhcp6_lease_set_fqdn(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + char *fqdn; + int r; + + assert(lease); + assert(optval || optlen == 0); + + if (optlen == 0) + return 0; + + if (optlen < 2) + return -ENODATA; + + /* Ignore the flags field, it doesn't carry any useful + information for clients. */ + r = dhcp6_option_parse_domainname(optval + 1, optlen - 1, &fqdn); + if (r < 0) + return r; + + return free_and_replace(lease->fqdn, fqdn); +} + +int sd_dhcp6_lease_get_fqdn(sd_dhcp6_lease *lease, const char **ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (!lease->fqdn) + return -ENODATA; + + *ret = lease->fqdn; + return 0; +} + +int dhcp6_lease_set_captive_portal(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + _cleanup_free_ char *uri = NULL; + int r; + + assert(lease); + assert(optval || optlen == 0); + + r = dhcp6_option_parse_string(optval, optlen, &uri); + if (r < 0) + return r; + + if (uri && !in_charset(uri, URI_VALID)) + return -EINVAL; + + return free_and_replace(lease->captive_portal, uri); +} + +int sd_dhcp6_lease_get_captive_portal(sd_dhcp6_lease *lease, const char **ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (!lease->captive_portal) + return -ENODATA; + + *ret = lease->captive_portal; + return 0; +} + +int sd_dhcp6_lease_get_vendor_options(sd_dhcp6_lease *lease, sd_dhcp6_option ***ret) { + int r; + + assert_return(lease, -EINVAL); + + if (set_isempty(lease->vendor_options)) + return -ENODATA; + + if (ret) { + if (!lease->sorted_vendor_options) { + r = set_dump_sorted(lease->vendor_options, (void***) &lease->sorted_vendor_options, NULL); + if (r < 0) + return r; + } + + *ret = lease->sorted_vendor_options; + } + + return set_size(lease->vendor_options); +} + +static int dhcp6_lease_insert_vendor_option( + sd_dhcp6_lease *lease, + uint16_t option_code, + const void *data, + size_t len, + uint32_t enterprise_id) { + + _cleanup_(sd_dhcp6_option_unrefp) sd_dhcp6_option *option = NULL; + + assert(lease); + + option = new(sd_dhcp6_option, 1); + if (!option) + return -ENOMEM; + + *option = (sd_dhcp6_option) { + .n_ref = 1, + .enterprise_identifier = enterprise_id, + .option = option_code, + .length = len, + }; + option->data = memdup_suffix0(data, len); + if (!option->data) + return -ENOMEM; + + return set_ensure_consume(&lease->vendor_options, &dhcp6_option_hash_ops, TAKE_PTR(option)); +} + +static int dhcp6_lease_add_vendor_option(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + int r; + uint32_t enterprise_id; + + assert(lease); + assert(optval || optlen == 0); + + if (optlen < sizeof(be32_t)) + return -EBADMSG; + + enterprise_id = unaligned_read_be32(optval); + + for (size_t offset = 4; offset < optlen;) { + const uint8_t *subval; + size_t sublen; + uint16_t subopt; + + r = dhcp6_option_parse(optval, optlen, &offset, &subopt, &sublen, &subval); + if (r < 0) + return r; + + r = dhcp6_lease_insert_vendor_option(lease, subopt, subval, sublen, enterprise_id); + if (r < 0) + return r; + } + return 0; +} + +static int dhcp6_lease_parse_message( + sd_dhcp6_client *client, + sd_dhcp6_lease *lease, + const DHCP6Message *message, + size_t len) { + + usec_t irt = IRT_DEFAULT; + int r; + + assert(client); + assert(lease); + assert(message); + assert(len >= sizeof(DHCP6Message)); + + len -= sizeof(DHCP6Message); + for (size_t offset = 0; offset < len;) { + uint16_t optcode; + size_t optlen; + const uint8_t *optval; + + if (len - offset < offsetof(DHCP6Option, data)) { + log_dhcp6_client(client, "Ignoring %zu invalid byte(s) at the end of the packet", len - offset); + break; + } + + r = dhcp6_option_parse(message->options, len, &offset, &optcode, &optlen, &optval); + if (r < 0) + return log_dhcp6_client_errno(client, r, + "Failed to parse option header at offset %zu of total length %zu: %m", + offset, len); + + switch (optcode) { + case SD_DHCP6_OPTION_CLIENTID: + if (dhcp6_lease_get_clientid(lease, NULL, NULL) >= 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), "%s contains multiple client IDs", + dhcp6_message_type_to_string(message->type)); + + r = dhcp6_lease_set_clientid(lease, optval, optlen); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set client ID: %m"); + + break; + + case SD_DHCP6_OPTION_SERVERID: + if (dhcp6_lease_get_serverid(lease, NULL, NULL) >= 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), "%s contains multiple server IDs", + dhcp6_message_type_to_string(message->type)); + + r = dhcp6_lease_set_serverid(lease, optval, optlen); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set server ID: %m"); + + break; + + case SD_DHCP6_OPTION_PREFERENCE: + if (optlen != 1) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), "Received invalid length for preference."); + + r = dhcp6_lease_set_preference(lease, optval[0]); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set preference: %m"); + + break; + + case SD_DHCP6_OPTION_STATUS_CODE: { + _cleanup_free_ char *msg = NULL; + + r = dhcp6_option_parse_status(optval, optlen, &msg); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to parse status code: %m"); + if (r > 0) + return log_dhcp6_client_errno(client, dhcp6_message_status_to_errno(r), + "Received %s message with non-zero status%s%s", + dhcp6_message_type_to_string(message->type), + isempty(msg) ? "." : ": ", strempty(msg)); + break; + } + case SD_DHCP6_OPTION_IA_NA: { + _cleanup_(dhcp6_ia_freep) DHCP6IA *ia = NULL; + + if (client->state == DHCP6_STATE_INFORMATION_REQUEST) { + log_dhcp6_client(client, "Ignoring IA NA option in information requesting mode."); + break; + } + + r = dhcp6_option_parse_ia(client, client->ia_na.header.id, optcode, optlen, optval, &ia); + if (r == -ENOMEM) + return log_oom_debug(); + if (r < 0) { + log_dhcp6_client_errno(client, r, "Failed to parse IA_NA option, ignoring: %m"); + continue; + } + + if (lease->ia_na) { + log_dhcp6_client(client, "Received duplicate matching IA_NA option, ignoring."); + continue; + } + + dhcp6_ia_free(lease->ia_na); + lease->ia_na = TAKE_PTR(ia); + break; + } + case SD_DHCP6_OPTION_IA_PD: { + _cleanup_(dhcp6_ia_freep) DHCP6IA *ia = NULL; + + if (client->state == DHCP6_STATE_INFORMATION_REQUEST) { + log_dhcp6_client(client, "Ignoring IA PD option in information requesting mode."); + break; + } + + r = dhcp6_option_parse_ia(client, client->ia_pd.header.id, optcode, optlen, optval, &ia); + if (r == -ENOMEM) + return log_oom_debug(); + if (r < 0) { + log_dhcp6_client_errno(client, r, "Failed to parse IA_PD option, ignoring: %m"); + continue; + } + + if (lease->ia_pd) { + log_dhcp6_client(client, "Received duplicate matching IA_PD option, ignoring."); + continue; + } + + dhcp6_ia_free(lease->ia_pd); + lease->ia_pd = TAKE_PTR(ia); + break; + } + case SD_DHCP6_OPTION_RAPID_COMMIT: + if (optlen != 0) + log_dhcp6_client(client, "Received rapid commit option with an invalid length (%zu), ignoring.", optlen); + + r = dhcp6_lease_set_rapid_commit(lease); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to set rapid commit flag: %m"); + + break; + + case SD_DHCP6_OPTION_DNS_SERVER: + r = dhcp6_lease_add_dns(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse DNS server option, ignoring: %m"); + + break; + + case SD_DHCP6_OPTION_DOMAIN: + r = dhcp6_lease_add_domains(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse domain list option, ignoring: %m"); + + break; + + case SD_DHCP6_OPTION_NTP_SERVER: + r = dhcp6_lease_add_ntp(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse NTP server option, ignoring: %m"); + + break; + + case SD_DHCP6_OPTION_SNTP_SERVER: + r = dhcp6_lease_add_sntp(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse SNTP server option, ignoring: %m"); + + break; + + case SD_DHCP6_OPTION_CAPTIVE_PORTAL: + r = dhcp6_lease_set_captive_portal(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse captive portal option, ignoring: %m"); + break; + + case SD_DHCP6_OPTION_CLIENT_FQDN: + r = dhcp6_lease_set_fqdn(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse FQDN option, ignoring: %m"); + + break; + + case SD_DHCP6_OPTION_INFORMATION_REFRESH_TIME: + if (optlen != 4) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "Received information refresh time option with an invalid length (%zu).", optlen); + + irt = unaligned_be32_sec_to_usec(optval, /* max_as_infinity = */ false); + break; + + case SD_DHCP6_OPTION_VENDOR_OPTS: + r = dhcp6_lease_add_vendor_option(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse vendor option, ignoring: %m"); + + break; + } + } + + uint8_t *clientid; + size_t clientid_len; + if (dhcp6_lease_get_clientid(lease, &clientid, &clientid_len) < 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "%s message does not contain client ID. Ignoring.", + dhcp6_message_type_to_string(message->type)); + + if (memcmp_nn(clientid, clientid_len, &client->duid, client->duid_len) != 0) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "The client ID in %s message does not match. Ignoring.", + dhcp6_message_type_to_string(message->type)); + + if (client->state == DHCP6_STATE_INFORMATION_REQUEST) { + client->information_refresh_time_usec = MAX(irt, IRT_MINIMUM); + log_dhcp6_client(client, "New information request will be refused in %s.", + FORMAT_TIMESPAN(client->information_refresh_time_usec, USEC_PER_SEC)); + + } else { + r = dhcp6_lease_get_serverid(lease, NULL, NULL); + if (r < 0) + return log_dhcp6_client_errno(client, r, "%s has no server id", + dhcp6_message_type_to_string(message->type)); + + if (!lease->ia_na && !lease->ia_pd) + return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL), + "No IA_PD prefix or IA_NA address received. Ignoring."); + + dhcp6_lease_set_lifetime(lease); + } + + return 0; +} + +static sd_dhcp6_lease *dhcp6_lease_free(sd_dhcp6_lease *lease) { + if (!lease) + return NULL; + + set_free(lease->vendor_options); + free(lease->sorted_vendor_options); + free(lease->clientid); + free(lease->serverid); + dhcp6_ia_free(lease->ia_na); + dhcp6_ia_free(lease->ia_pd); + free(lease->dns); + free(lease->fqdn); + free(lease->captive_portal); + strv_free(lease->domains); + free(lease->ntp); + strv_free(lease->ntp_fqdn); + free(lease->sntp); + + return mfree(lease); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp6_lease, sd_dhcp6_lease, dhcp6_lease_free); + +int dhcp6_lease_new(sd_dhcp6_lease **ret) { + sd_dhcp6_lease *lease; + + assert(ret); + + lease = new(sd_dhcp6_lease, 1); + if (!lease) + return -ENOMEM; + + *lease = (sd_dhcp6_lease) { + .n_ref = 1, + }; + + *ret = lease; + return 0; +} + +int dhcp6_lease_new_from_message( + sd_dhcp6_client *client, + const DHCP6Message *message, + size_t len, + const triple_timestamp *timestamp, + const struct in6_addr *server_address, + sd_dhcp6_lease **ret) { + + _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease = NULL; + int r; + + assert(client); + assert(message); + assert(len >= sizeof(DHCP6Message)); + assert(ret); + + r = dhcp6_lease_new(&lease); + if (r < 0) + return r; + + dhcp6_lease_set_timestamp(lease, timestamp); + dhcp6_lease_set_server_address(lease, server_address); + + r = dhcp6_lease_parse_message(client, lease, message, len); + if (r < 0) + return r; + + *ret = TAKE_PTR(lease); + return 0; +} diff --git a/src/libsystemd-network/sd-ipv4acd.c b/src/libsystemd-network/sd-ipv4acd.c new file mode 100644 index 0000000..0cc37a6 --- /dev/null +++ b/src/libsystemd-network/sd-ipv4acd.c @@ -0,0 +1,617 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Axis Communications AB. All rights reserved. +***/ + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/if_ether.h> +#include <stdio.h> +#include <stdlib.h> + +#include "sd-ipv4acd.h" + +#include "alloc-util.h" +#include "arp-util.h" +#include "ether-addr-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "in-addr-util.h" +#include "memory-util.h" +#include "network-common.h" +#include "random-util.h" +#include "siphash24.h" +#include "string-table.h" +#include "string-util.h" +#include "time-util.h" + +/* Constants from the RFC */ +#define PROBE_WAIT_USEC (1U * USEC_PER_SEC) +#define PROBE_NUM 3U +#define PROBE_MIN_USEC (1U * USEC_PER_SEC) +#define PROBE_MAX_USEC (2U * USEC_PER_SEC) +#define ANNOUNCE_WAIT_USEC (2U * USEC_PER_SEC) +#define ANNOUNCE_NUM 2U +#define ANNOUNCE_INTERVAL_USEC (2U * USEC_PER_SEC) +#define MAX_CONFLICTS 10U +#define RATE_LIMIT_INTERVAL_USEC (60U * USEC_PER_SEC) +#define DEFEND_INTERVAL_USEC (10U * USEC_PER_SEC) + +typedef enum IPv4ACDState { + IPV4ACD_STATE_INIT, + IPV4ACD_STATE_STARTED, + IPV4ACD_STATE_WAITING_PROBE, + IPV4ACD_STATE_PROBING, + IPV4ACD_STATE_WAITING_ANNOUNCE, + IPV4ACD_STATE_ANNOUNCING, + IPV4ACD_STATE_RUNNING, + _IPV4ACD_STATE_MAX, + _IPV4ACD_STATE_INVALID = -EINVAL, +} IPv4ACDState; + +struct sd_ipv4acd { + unsigned n_ref; + + IPv4ACDState state; + int ifindex; + int fd; + + char *ifname; + unsigned n_iteration; + unsigned n_conflict; + + sd_event_source *receive_message_event_source; + sd_event_source *timer_event_source; + + usec_t defend_window; + struct in_addr address; + + /* External */ + struct ether_addr mac_addr; + + sd_event *event; + int event_priority; + sd_ipv4acd_callback_t callback; + void *userdata; + sd_ipv4acd_check_mac_callback_t check_mac_callback; + void *check_mac_userdata; +}; + +#define log_ipv4acd_errno(acd, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "IPv4ACD: ", \ + sd_ipv4acd, acd, \ + error, fmt, ##__VA_ARGS__) +#define log_ipv4acd(acd, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "IPv4ACD: ", \ + sd_ipv4acd, acd, \ + 0, fmt, ##__VA_ARGS__) + +static const char * const ipv4acd_state_table[_IPV4ACD_STATE_MAX] = { + [IPV4ACD_STATE_INIT] = "init", + [IPV4ACD_STATE_STARTED] = "started", + [IPV4ACD_STATE_WAITING_PROBE] = "waiting-probe", + [IPV4ACD_STATE_PROBING] = "probing", + [IPV4ACD_STATE_WAITING_ANNOUNCE] = "waiting-announce", + [IPV4ACD_STATE_ANNOUNCING] = "announcing", + [IPV4ACD_STATE_RUNNING] = "running", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(ipv4acd_state, IPv4ACDState); + +static void ipv4acd_set_state(sd_ipv4acd *acd, IPv4ACDState st, bool reset_counter) { + assert(acd); + assert(st < _IPV4ACD_STATE_MAX); + + if (st != acd->state) + log_ipv4acd(acd, "%s -> %s", ipv4acd_state_to_string(acd->state), ipv4acd_state_to_string(st)); + + if (st == acd->state && !reset_counter) + acd->n_iteration++; + else { + acd->state = st; + acd->n_iteration = 0; + } +} + +static void ipv4acd_reset(sd_ipv4acd *acd) { + assert(acd); + + (void) event_source_disable(acd->timer_event_source); + acd->receive_message_event_source = sd_event_source_disable_unref(acd->receive_message_event_source); + + acd->fd = safe_close(acd->fd); + + ipv4acd_set_state(acd, IPV4ACD_STATE_INIT, true); +} + +static sd_ipv4acd *ipv4acd_free(sd_ipv4acd *acd) { + assert(acd); + + ipv4acd_reset(acd); + sd_event_source_unref(acd->timer_event_source); + sd_ipv4acd_detach_event(acd); + free(acd->ifname); + return mfree(acd); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_ipv4acd, sd_ipv4acd, ipv4acd_free); + +int sd_ipv4acd_new(sd_ipv4acd **ret) { + _cleanup_(sd_ipv4acd_unrefp) sd_ipv4acd *acd = NULL; + + assert_return(ret, -EINVAL); + + acd = new(sd_ipv4acd, 1); + if (!acd) + return -ENOMEM; + + *acd = (sd_ipv4acd) { + .n_ref = 1, + .state = IPV4ACD_STATE_INIT, + .ifindex = -1, + .fd = -EBADF, + }; + + *ret = TAKE_PTR(acd); + + return 0; +} + +static void ipv4acd_client_notify(sd_ipv4acd *acd, int event) { + assert(acd); + + if (!acd->callback) + return; + + acd->callback(acd, event, acd->userdata); +} + +int sd_ipv4acd_stop(sd_ipv4acd *acd) { + IPv4ACDState old_state; + + if (!acd) + return 0; + + old_state = acd->state; + + ipv4acd_reset(acd); + + if (old_state == IPV4ACD_STATE_INIT) + return 0; + + log_ipv4acd(acd, "STOPPED"); + + ipv4acd_client_notify(acd, SD_IPV4ACD_EVENT_STOP); + + return 0; +} + +static int ipv4acd_on_timeout(sd_event_source *s, uint64_t usec, void *userdata); + +static int ipv4acd_set_next_wakeup(sd_ipv4acd *acd, usec_t usec, usec_t random_usec) { + usec_t next_timeout, time_now; + + assert(acd); + + next_timeout = usec; + + if (random_usec > 0) + next_timeout += (usec_t) random_u64() % random_usec; + + assert_se(sd_event_now(acd->event, CLOCK_BOOTTIME, &time_now) >= 0); + + return event_reset_time(acd->event, &acd->timer_event_source, + CLOCK_BOOTTIME, + time_now + next_timeout, 0, + ipv4acd_on_timeout, acd, + acd->event_priority, "ipv4acd-timer", true); +} + +static int ipv4acd_on_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + sd_ipv4acd *acd = ASSERT_PTR(userdata); + int r = 0; + + switch (acd->state) { + + case IPV4ACD_STATE_STARTED: + acd->defend_window = 0; + + ipv4acd_set_state(acd, IPV4ACD_STATE_WAITING_PROBE, true); + + if (acd->n_conflict >= MAX_CONFLICTS) { + log_ipv4acd(acd, "Max conflicts reached, delaying by %s", + FORMAT_TIMESPAN(RATE_LIMIT_INTERVAL_USEC, 0)); + r = ipv4acd_set_next_wakeup(acd, RATE_LIMIT_INTERVAL_USEC, PROBE_WAIT_USEC); + } else + r = ipv4acd_set_next_wakeup(acd, 0, PROBE_WAIT_USEC); + if (r < 0) + goto fail; + + break; + + case IPV4ACD_STATE_WAITING_PROBE: + case IPV4ACD_STATE_PROBING: + /* Send a probe */ + r = arp_send_probe(acd->fd, acd->ifindex, &acd->address, &acd->mac_addr); + if (r < 0) { + log_ipv4acd_errno(acd, r, "Failed to send ARP probe: %m"); + goto fail; + } + + log_ipv4acd(acd, "Probing "IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(acd->address)); + + if (acd->n_iteration < PROBE_NUM - 2) { + ipv4acd_set_state(acd, IPV4ACD_STATE_PROBING, false); + + r = ipv4acd_set_next_wakeup(acd, PROBE_MIN_USEC, (PROBE_MAX_USEC-PROBE_MIN_USEC)); + if (r < 0) + goto fail; + } else { + ipv4acd_set_state(acd, IPV4ACD_STATE_WAITING_ANNOUNCE, true); + + r = ipv4acd_set_next_wakeup(acd, ANNOUNCE_WAIT_USEC, 0); + if (r < 0) + goto fail; + } + + break; + + case IPV4ACD_STATE_ANNOUNCING: + if (acd->n_iteration >= ANNOUNCE_NUM - 1) { + ipv4acd_set_state(acd, IPV4ACD_STATE_RUNNING, false); + break; + } + + _fallthrough_; + case IPV4ACD_STATE_WAITING_ANNOUNCE: + /* Send announcement packet */ + r = arp_send_announcement(acd->fd, acd->ifindex, &acd->address, &acd->mac_addr); + if (r < 0) { + log_ipv4acd_errno(acd, r, "Failed to send ARP announcement: %m"); + goto fail; + } + + log_ipv4acd(acd, "Announcing "IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(acd->address)); + + ipv4acd_set_state(acd, IPV4ACD_STATE_ANNOUNCING, false); + + r = ipv4acd_set_next_wakeup(acd, ANNOUNCE_INTERVAL_USEC, 0); + if (r < 0) + goto fail; + + if (acd->n_iteration == 0) { + acd->n_conflict = 0; + ipv4acd_client_notify(acd, SD_IPV4ACD_EVENT_BIND); + } + + break; + + default: + assert_not_reached(); + } + + return 0; + +fail: + sd_ipv4acd_stop(acd); + return 0; +} + +static bool ipv4acd_arp_conflict(sd_ipv4acd *acd, const struct ether_arp *arp, bool announced) { + assert(acd); + assert(arp); + + /* RFC 5227 section 2.1.1. + * "the host receives any ARP packet (Request *or* Reply) on the interface where the probe is + * being performed, where the packet's 'sender IP address' is the address being probed for, + * then the host MUST treat this address as being in use by some other host" */ + if (memcmp(arp->arp_spa, &acd->address, sizeof(struct in_addr)) == 0) + return true; + + if (announced) + /* the TPA matched instead of SPA, this is not a conflict */ + return false; + + /* "any ARP Probe where the packet's 'target IP address' is the address being probed for, and + * the packet's 'sender hardware address' is not the hardware address of any of the host's + * interfaces, then the host SHOULD similarly treat this as an address conflict" */ + if (arp->ea_hdr.ar_op != htobe16(ARPOP_REQUEST)) + return false; /* not ARP Request, ignoring. */ + if (memeqzero(arp->arp_spa, sizeof(struct in_addr)) == 0) + return false; /* not ARP Probe, ignoring. */ + if (memcmp(arp->arp_tpa, &acd->address, sizeof(struct in_addr)) != 0) + return false; /* target IP address does not match, BPF code is broken? */ + + if (acd->check_mac_callback && + acd->check_mac_callback(acd, (const struct ether_addr*) arp->arp_sha, acd->check_mac_userdata) > 0) + /* sender hardware is one of the host's interfaces, ignoring. */ + return false; + + return true; /* conflict! */ +} + +static void ipv4acd_on_conflict(sd_ipv4acd *acd) { + assert(acd); + + acd->n_conflict++; + + log_ipv4acd(acd, "Conflict on "IPV4_ADDRESS_FMT_STR" (%u)", IPV4_ADDRESS_FMT_VAL(acd->address), acd->n_conflict); + + ipv4acd_reset(acd); + ipv4acd_client_notify(acd, SD_IPV4ACD_EVENT_CONFLICT); +} + +static int ipv4acd_on_packet( + sd_event_source *s, + int fd, + uint32_t revents, + void *userdata) { + + sd_ipv4acd *acd = ASSERT_PTR(userdata); + struct ether_arp packet; + ssize_t n; + int r; + + assert(s); + assert(fd >= 0); + + n = recv(fd, &packet, sizeof(struct ether_arp), 0); + if (n < 0) { + if (ERRNO_IS_TRANSIENT(errno) || ERRNO_IS_DISCONNECT(errno)) + return 0; + + log_ipv4acd_errno(acd, errno, "Failed to read ARP packet: %m"); + goto fail; + } + if ((size_t) n != sizeof(struct ether_arp)) { + log_ipv4acd(acd, "Ignoring too short ARP packet."); + return 0; + } + + switch (acd->state) { + + case IPV4ACD_STATE_ANNOUNCING: + case IPV4ACD_STATE_RUNNING: + + if (ipv4acd_arp_conflict(acd, &packet, true)) { + usec_t ts; + + assert_se(sd_event_now(acd->event, CLOCK_BOOTTIME, &ts) >= 0); + + /* Defend address */ + if (ts > acd->defend_window) { + acd->defend_window = ts + DEFEND_INTERVAL_USEC; + r = arp_send_announcement(acd->fd, acd->ifindex, &acd->address, &acd->mac_addr); + if (r < 0) { + log_ipv4acd_errno(acd, r, "Failed to send ARP announcement: %m"); + goto fail; + } + + log_ipv4acd(acd, "Defending "IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(acd->address)); + + } else + ipv4acd_on_conflict(acd); + } + break; + + case IPV4ACD_STATE_WAITING_PROBE: + case IPV4ACD_STATE_PROBING: + case IPV4ACD_STATE_WAITING_ANNOUNCE: + if (ipv4acd_arp_conflict(acd, &packet, false)) + ipv4acd_on_conflict(acd); + break; + + default: + assert_not_reached(); + } + + return 0; + +fail: + sd_ipv4acd_stop(acd); + return 0; +} + +int sd_ipv4acd_set_ifindex(sd_ipv4acd *acd, int ifindex) { + assert_return(acd, -EINVAL); + assert_return(ifindex > 0, -EINVAL); + assert_return(acd->state == IPV4ACD_STATE_INIT, -EBUSY); + + acd->ifindex = ifindex; + + return 0; +} + +int sd_ipv4acd_get_ifindex(sd_ipv4acd *acd) { + if (!acd) + return -EINVAL; + + return acd->ifindex; +} + +int sd_ipv4acd_set_ifname(sd_ipv4acd *acd, const char *ifname) { + assert_return(acd, -EINVAL); + assert_return(ifname, -EINVAL); + + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) + return -EINVAL; + + return free_and_strdup(&acd->ifname, ifname); +} + +int sd_ipv4acd_get_ifname(sd_ipv4acd *acd, const char **ret) { + int r; + + assert_return(acd, -EINVAL); + + r = get_ifname(acd->ifindex, &acd->ifname); + if (r < 0) + return r; + + if (ret) + *ret = acd->ifname; + + return 0; +} + +int sd_ipv4acd_set_mac(sd_ipv4acd *acd, const struct ether_addr *addr) { + int r; + + assert_return(acd, -EINVAL); + assert_return(addr, -EINVAL); + assert_return(!ether_addr_is_null(addr), -EINVAL); + + acd->mac_addr = *addr; + + if (!sd_ipv4acd_is_running(acd)) + return 0; + + assert(acd->fd >= 0); + r = arp_update_filter(acd->fd, &acd->address, &acd->mac_addr); + if (r < 0) { + ipv4acd_reset(acd); + return r; + } + + return 0; +} + +int sd_ipv4acd_detach_event(sd_ipv4acd *acd) { + assert_return(acd, -EINVAL); + + acd->event = sd_event_unref(acd->event); + + return 0; +} + +int sd_ipv4acd_attach_event(sd_ipv4acd *acd, sd_event *event, int64_t priority) { + int r; + + assert_return(acd, -EINVAL); + assert_return(!acd->event, -EBUSY); + + if (event) + acd->event = sd_event_ref(event); + else { + r = sd_event_default(&acd->event); + if (r < 0) + return r; + } + + acd->event_priority = priority; + + return 0; +} + +int sd_ipv4acd_set_callback(sd_ipv4acd *acd, sd_ipv4acd_callback_t cb, void *userdata) { + assert_return(acd, -EINVAL); + + acd->callback = cb; + acd->userdata = userdata; + + return 0; +} + +int sd_ipv4acd_set_check_mac_callback(sd_ipv4acd *acd, sd_ipv4acd_check_mac_callback_t cb, void *userdata) { + assert_return(acd, -EINVAL); + + acd->check_mac_callback = cb; + acd->check_mac_userdata = userdata; + return 0; +} + +int sd_ipv4acd_set_address(sd_ipv4acd *acd, const struct in_addr *address) { + int r; + + assert_return(acd, -EINVAL); + assert_return(address, -EINVAL); + assert_return(in4_addr_is_set(address), -EINVAL); + + if (in4_addr_equal(&acd->address, address)) + return 0; + + acd->address = *address; + + if (!sd_ipv4acd_is_running(acd)) + return 0; + + assert(acd->fd >= 0); + r = arp_update_filter(acd->fd, &acd->address, &acd->mac_addr); + if (r < 0) + goto fail; + + r = ipv4acd_set_next_wakeup(acd, 0, 0); + if (r < 0) + goto fail; + + ipv4acd_set_state(acd, IPV4ACD_STATE_STARTED, true); + return 0; + +fail: + ipv4acd_reset(acd); + return r; +} + +int sd_ipv4acd_get_address(sd_ipv4acd *acd, struct in_addr *address) { + assert_return(acd, -EINVAL); + assert_return(address, -EINVAL); + + *address = acd->address; + + return 0; +} + +int sd_ipv4acd_is_running(sd_ipv4acd *acd) { + assert_return(acd, false); + + return acd->state != IPV4ACD_STATE_INIT; +} + +int sd_ipv4acd_is_bound(sd_ipv4acd *acd) { + assert_return(acd, false); + + return IN_SET(acd->state, IPV4ACD_STATE_ANNOUNCING, IPV4ACD_STATE_RUNNING); +} + +int sd_ipv4acd_start(sd_ipv4acd *acd, bool reset_conflicts) { + int r; + + assert_return(acd, -EINVAL); + assert_return(acd->event, -EINVAL); + assert_return(acd->ifindex > 0, -EINVAL); + assert_return(in4_addr_is_set(&acd->address), -EINVAL); + assert_return(!ether_addr_is_null(&acd->mac_addr), -EINVAL); + assert_return(acd->state == IPV4ACD_STATE_INIT, -EBUSY); + + r = arp_network_bind_raw_socket(acd->ifindex, &acd->address, &acd->mac_addr); + if (r < 0) + return r; + + close_and_replace(acd->fd, r); + + if (reset_conflicts) + acd->n_conflict = 0; + + r = sd_event_add_io(acd->event, &acd->receive_message_event_source, acd->fd, EPOLLIN, ipv4acd_on_packet, acd); + if (r < 0) + goto fail; + + r = sd_event_source_set_priority(acd->receive_message_event_source, acd->event_priority); + if (r < 0) + goto fail; + + (void) sd_event_source_set_description(acd->receive_message_event_source, "ipv4acd-receive-message"); + + r = ipv4acd_set_next_wakeup(acd, 0, 0); + if (r < 0) + goto fail; + + ipv4acd_set_state(acd, IPV4ACD_STATE_STARTED, true); + return 0; + +fail: + ipv4acd_reset(acd); + return r; +} diff --git a/src/libsystemd-network/sd-ipv4ll.c b/src/libsystemd-network/sd-ipv4ll.c new file mode 100644 index 0000000..a29279e --- /dev/null +++ b/src/libsystemd-network/sd-ipv4ll.c @@ -0,0 +1,365 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Axis Communications AB. All rights reserved. +***/ + +#include <arpa/inet.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> + +#include "sd-id128.h" +#include "sd-ipv4acd.h" +#include "sd-ipv4ll.h" + +#include "alloc-util.h" +#include "ether-addr-util.h" +#include "in-addr-util.h" +#include "network-common.h" +#include "random-util.h" +#include "siphash24.h" +#include "sparse-endian.h" +#include "string-util.h" + +#define IPV4LL_NETWORK UINT32_C(0xA9FE0000) +#define IPV4LL_NETMASK UINT32_C(0xFFFF0000) + +#define IPV4LL_DONT_DESTROY(ll) \ + _cleanup_(sd_ipv4ll_unrefp) _unused_ sd_ipv4ll *_dont_destroy_##ll = sd_ipv4ll_ref(ll) + +struct sd_ipv4ll { + unsigned n_ref; + + sd_ipv4acd *acd; + + be32_t address; /* the address pushed to ACD */ + struct ether_addr mac; + + struct { + le64_t value; + le64_t generation; + } seed; + bool seed_set; + + /* External */ + be32_t claimed_address; + + sd_ipv4ll_callback_t callback; + void *userdata; + + sd_ipv4ll_check_mac_callback_t check_mac_callback; + void *check_mac_userdata; +}; + +#define log_ipv4ll_errno(ll, error, fmt, ...) \ + log_interface_prefix_full_errno( \ + "IPv4LL: ", \ + sd_ipv4ll, ll, \ + error, fmt, ##__VA_ARGS__) +#define log_ipv4ll(ll, fmt, ...) \ + log_interface_prefix_full_errno_zerook( \ + "IPv4LL: ", \ + sd_ipv4ll, ll, \ + 0, fmt, ##__VA_ARGS__) + +static void ipv4ll_on_acd(sd_ipv4acd *acd, int event, void *userdata); +static int ipv4ll_check_mac(sd_ipv4acd *acd, const struct ether_addr *mac, void *userdata); + +static sd_ipv4ll *ipv4ll_free(sd_ipv4ll *ll) { + assert(ll); + + sd_ipv4acd_unref(ll->acd); + return mfree(ll); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_ipv4ll, sd_ipv4ll, ipv4ll_free); + +int sd_ipv4ll_new(sd_ipv4ll **ret) { + _cleanup_(sd_ipv4ll_unrefp) sd_ipv4ll *ll = NULL; + int r; + + assert_return(ret, -EINVAL); + + ll = new0(sd_ipv4ll, 1); + if (!ll) + return -ENOMEM; + + ll->n_ref = 1; + + r = sd_ipv4acd_new(&ll->acd); + if (r < 0) + return r; + + r = sd_ipv4acd_set_callback(ll->acd, ipv4ll_on_acd, ll); + if (r < 0) + return r; + + r = sd_ipv4acd_set_check_mac_callback(ll->acd, ipv4ll_check_mac, ll); + if (r < 0) + return r; + + *ret = TAKE_PTR(ll); + + return 0; +} + +int sd_ipv4ll_stop(sd_ipv4ll *ll) { + if (!ll) + return 0; + + return sd_ipv4acd_stop(ll->acd); +} + +int sd_ipv4ll_set_ifindex(sd_ipv4ll *ll, int ifindex) { + assert_return(ll, -EINVAL); + assert_return(ifindex > 0, -EINVAL); + assert_return(sd_ipv4ll_is_running(ll) == 0, -EBUSY); + + return sd_ipv4acd_set_ifindex(ll->acd, ifindex); +} + +int sd_ipv4ll_get_ifindex(sd_ipv4ll *ll) { + if (!ll) + return -EINVAL; + + return sd_ipv4acd_get_ifindex(ll->acd); +} + +int sd_ipv4ll_set_ifname(sd_ipv4ll *ll, const char *ifname) { + assert_return(ll, -EINVAL); + assert_return(ifname, -EINVAL); + + return sd_ipv4acd_set_ifname(ll->acd, ifname); +} + +int sd_ipv4ll_get_ifname(sd_ipv4ll *ll, const char **ret) { + assert_return(ll, -EINVAL); + + return sd_ipv4acd_get_ifname(ll->acd, ret); +} + +int sd_ipv4ll_set_mac(sd_ipv4ll *ll, const struct ether_addr *addr) { + int r; + + assert_return(ll, -EINVAL); + assert_return(addr, -EINVAL); + assert_return(!ether_addr_is_null(addr), -EINVAL); + + r = sd_ipv4acd_set_mac(ll->acd, addr); + if (r < 0) + return r; + + ll->mac = *addr; + return 0; +} + +int sd_ipv4ll_detach_event(sd_ipv4ll *ll) { + assert_return(ll, -EINVAL); + + return sd_ipv4acd_detach_event(ll->acd); +} + +int sd_ipv4ll_attach_event(sd_ipv4ll *ll, sd_event *event, int64_t priority) { + assert_return(ll, -EINVAL); + + return sd_ipv4acd_attach_event(ll->acd, event, priority); +} + +int sd_ipv4ll_set_callback(sd_ipv4ll *ll, sd_ipv4ll_callback_t cb, void *userdata) { + assert_return(ll, -EINVAL); + + ll->callback = cb; + ll->userdata = userdata; + + return 0; +} + +int sd_ipv4ll_set_check_mac_callback(sd_ipv4ll *ll, sd_ipv4ll_check_mac_callback_t cb, void *userdata) { + assert_return(ll, -EINVAL); + + ll->check_mac_callback = cb; + ll->check_mac_userdata = userdata; + + return 0; +} + +int sd_ipv4ll_get_address(sd_ipv4ll *ll, struct in_addr *address) { + assert_return(ll, -EINVAL); + assert_return(address, -EINVAL); + + if (ll->claimed_address == 0) + return -ENOENT; + + address->s_addr = ll->claimed_address; + + return 0; +} + +int sd_ipv4ll_set_address_seed(sd_ipv4ll *ll, uint64_t seed) { + assert_return(ll, -EINVAL); + assert_return(sd_ipv4ll_is_running(ll) == 0, -EBUSY); + + ll->seed.value = htole64(seed); + ll->seed_set = true; + + return 0; +} + +int sd_ipv4ll_is_running(sd_ipv4ll *ll) { + assert_return(ll, false); + + return sd_ipv4acd_is_running(ll->acd); +} + +int sd_ipv4ll_set_address(sd_ipv4ll *ll, const struct in_addr *address) { + int r; + + assert_return(ll, -EINVAL); + assert_return(address, -EINVAL); + assert_return(in4_addr_is_link_local_dynamic(address), -EINVAL); + + r = sd_ipv4acd_set_address(ll->acd, address); + if (r < 0) + return r; + + ll->address = address->s_addr; + + return 0; +} + +#define PICK_HASH_KEY SD_ID128_MAKE(15,ac,82,a6,d6,3f,49,78,98,77,5d,0c,69,02,94,0b) + +static int ipv4ll_pick_address(sd_ipv4ll *ll) { + be32_t addr; + + assert(ll); + + do { + uint64_t h; + + h = siphash24(&ll->seed, sizeof(ll->seed), PICK_HASH_KEY.bytes); + + /* Increase the generation counter by one */ + ll->seed.generation = htole64(le64toh(ll->seed.generation) + 1); + + addr = htobe32((h & UINT32_C(0x0000FFFF)) | IPV4LL_NETWORK); + } while (addr == ll->address || + IN_SET(be32toh(addr) & 0x0000FF00U, 0x0000U, 0xFF00U)); + + log_ipv4ll(ll, "Picked new IP address %s.", IN4_ADDR_TO_STRING((const struct in_addr*) &addr)); + + return sd_ipv4ll_set_address(ll, &(struct in_addr) { addr }); +} + +#define MAC_HASH_KEY SD_ID128_MAKE(df,04,22,98,3f,ad,14,52,f9,87,2e,d1,9c,70,e2,f2) + +static int ipv4ll_start_internal(sd_ipv4ll *ll, bool reset_generation) { + int r; + bool picked_address = false; + + assert_return(ll, -EINVAL); + assert_return(!ether_addr_is_null(&ll->mac), -EINVAL); + + /* If no random seed is set, generate some from the MAC address */ + if (!ll->seed_set) + ll->seed.value = htole64(siphash24(ll->mac.ether_addr_octet, ETH_ALEN, MAC_HASH_KEY.bytes)); + + if (reset_generation) + ll->seed.generation = 0; + + if (ll->address == 0) { + r = ipv4ll_pick_address(ll); + if (r < 0) + return r; + + picked_address = true; + } + + r = sd_ipv4acd_start(ll->acd, reset_generation); + if (r < 0) { + + /* We couldn't start? If so, let's forget the picked address again, the user might make a change and + * retry, and we want the new data to take effect when picking an address. */ + if (picked_address) + ll->address = 0; + + return r; + } + + return 1; +} + +int sd_ipv4ll_start(sd_ipv4ll *ll) { + assert_return(ll, -EINVAL); + + if (sd_ipv4ll_is_running(ll)) + return 0; + + return ipv4ll_start_internal(ll, true); +} + +int sd_ipv4ll_restart(sd_ipv4ll *ll) { + ll->address = 0; + + return ipv4ll_start_internal(ll, false); +} + +static void ipv4ll_client_notify(sd_ipv4ll *ll, int event) { + assert(ll); + + if (ll->callback) + ll->callback(ll, event, ll->userdata); +} + +void ipv4ll_on_acd(sd_ipv4acd *acd, int event, void *userdata) { + sd_ipv4ll *ll = ASSERT_PTR(userdata); + IPV4LL_DONT_DESTROY(ll); + int r; + + assert(acd); + + switch (event) { + + case SD_IPV4ACD_EVENT_STOP: + ipv4ll_client_notify(ll, SD_IPV4LL_EVENT_STOP); + ll->claimed_address = 0; + break; + + case SD_IPV4ACD_EVENT_BIND: + ll->claimed_address = ll->address; + ipv4ll_client_notify(ll, SD_IPV4LL_EVENT_BIND); + break; + + case SD_IPV4ACD_EVENT_CONFLICT: + /* if an address was already bound we must call up to the + user to handle this, otherwise we just try again */ + if (ll->claimed_address != 0) { + ipv4ll_client_notify(ll, SD_IPV4LL_EVENT_CONFLICT); + + ll->claimed_address = 0; + } else { + r = sd_ipv4ll_restart(ll); + if (r < 0) + goto error; + } + + break; + + default: + assert_not_reached(); + } + + return; + +error: + ipv4ll_client_notify(ll, SD_IPV4LL_EVENT_STOP); +} + +static int ipv4ll_check_mac(sd_ipv4acd *acd, const struct ether_addr *mac, void *userdata) { + sd_ipv4ll *ll = ASSERT_PTR(userdata); + + if (ll->check_mac_callback) + return ll->check_mac_callback(ll, mac, ll->check_mac_userdata); + + return 0; +} diff --git a/src/libsystemd-network/sd-lldp-rx.c b/src/libsystemd-network/sd-lldp-rx.c new file mode 100644 index 0000000..2fc9a55 --- /dev/null +++ b/src/libsystemd-network/sd-lldp-rx.c @@ -0,0 +1,524 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <linux/sockios.h> +#include <sys/ioctl.h> + +#include "sd-lldp-rx.h" + +#include "alloc-util.h" +#include "ether-addr-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "lldp-neighbor.h" +#include "lldp-network.h" +#include "lldp-rx-internal.h" +#include "memory-util.h" +#include "network-common.h" +#include "socket-util.h" +#include "sort-util.h" +#include "string-table.h" + +#define LLDP_DEFAULT_NEIGHBORS_MAX 128U + +static const char * const lldp_rx_event_table[_SD_LLDP_RX_EVENT_MAX] = { + [SD_LLDP_RX_EVENT_ADDED] = "added", + [SD_LLDP_RX_EVENT_REMOVED] = "removed", + [SD_LLDP_RX_EVENT_UPDATED] = "updated", + [SD_LLDP_RX_EVENT_REFRESHED] = "refreshed", +}; + +DEFINE_STRING_TABLE_LOOKUP(lldp_rx_event, sd_lldp_rx_event_t); + +static void lldp_rx_flush_neighbors(sd_lldp_rx *lldp_rx) { + assert(lldp_rx); + + hashmap_clear(lldp_rx->neighbor_by_id); +} + +static void lldp_rx_callback(sd_lldp_rx *lldp_rx, sd_lldp_rx_event_t event, sd_lldp_neighbor *n) { + assert(lldp_rx); + assert(event >= 0 && event < _SD_LLDP_RX_EVENT_MAX); + + if (!lldp_rx->callback) + return (void) log_lldp_rx(lldp_rx, "Received '%s' event.", lldp_rx_event_to_string(event)); + + log_lldp_rx(lldp_rx, "Invoking callback for '%s' event.", lldp_rx_event_to_string(event)); + lldp_rx->callback(lldp_rx, event, n, lldp_rx->userdata); +} + +static int lldp_rx_make_space(sd_lldp_rx *lldp_rx, size_t extra) { + usec_t t = USEC_INFINITY; + bool changed = false; + + assert(lldp_rx); + + /* Remove all entries that are past their TTL, and more until at least the specified number of extra entries + * are free. */ + + for (;;) { + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL; + + n = prioq_peek(lldp_rx->neighbor_by_expiry); + if (!n) + break; + + sd_lldp_neighbor_ref(n); + + if (hashmap_size(lldp_rx->neighbor_by_id) > LESS_BY(lldp_rx->neighbors_max, extra)) + goto remove_one; + + if (t == USEC_INFINITY) + t = now(CLOCK_BOOTTIME); + + if (n->until > t) + break; + + remove_one: + lldp_neighbor_unlink(n); + lldp_rx_callback(lldp_rx, SD_LLDP_RX_EVENT_REMOVED, n); + changed = true; + } + + return changed; +} + +static bool lldp_rx_keep_neighbor(sd_lldp_rx *lldp_rx, sd_lldp_neighbor *n) { + assert(lldp_rx); + assert(n); + + /* Don't keep data with a zero TTL */ + if (n->ttl <= 0) + return false; + + /* Filter out data from the filter address */ + if (!ether_addr_is_null(&lldp_rx->filter_address) && + ether_addr_equal(&lldp_rx->filter_address, &n->source_address)) + return false; + + /* Only add if the neighbor has a capability we are interested in. Note that we also store all neighbors with + * no caps field set. */ + if (n->has_capabilities && + (n->enabled_capabilities & lldp_rx->capability_mask) == 0) + return false; + + /* Keep everything else */ + return true; +} + +static int lldp_rx_start_timer(sd_lldp_rx *lldp_rx, sd_lldp_neighbor *neighbor); + +static int lldp_rx_add_neighbor(sd_lldp_rx *lldp_rx, sd_lldp_neighbor *n) { + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *old = NULL; + bool keep; + int r; + + assert(lldp_rx); + assert(n); + assert(!n->lldp_rx); + + keep = lldp_rx_keep_neighbor(lldp_rx, n); + + /* First retrieve the old entry for this MSAP */ + old = hashmap_get(lldp_rx->neighbor_by_id, &n->id); + if (old) { + sd_lldp_neighbor_ref(old); + + if (!keep) { + lldp_neighbor_unlink(old); + lldp_rx_callback(lldp_rx, SD_LLDP_RX_EVENT_REMOVED, old); + return 0; + } + + if (lldp_neighbor_equal(n, old)) { + /* Is this equal, then restart the TTL counter, but don't do anything else. */ + old->timestamp = n->timestamp; + lldp_rx_start_timer(lldp_rx, old); + lldp_rx_callback(lldp_rx, SD_LLDP_RX_EVENT_REFRESHED, old); + return 0; + } + + /* Data changed, remove the old entry, and add a new one */ + lldp_neighbor_unlink(old); + + } else if (!keep) + return 0; + + /* Then, make room for at least one new neighbor */ + lldp_rx_make_space(lldp_rx, 1); + + r = hashmap_ensure_put(&lldp_rx->neighbor_by_id, &lldp_neighbor_hash_ops, &n->id, n); + if (r < 0) + goto finish; + + r = prioq_ensure_put(&lldp_rx->neighbor_by_expiry, lldp_neighbor_prioq_compare_func, n, &n->prioq_idx); + if (r < 0) { + assert_se(hashmap_remove(lldp_rx->neighbor_by_id, &n->id) == n); + goto finish; + } + + n->lldp_rx = lldp_rx; + + lldp_rx_start_timer(lldp_rx, n); + lldp_rx_callback(lldp_rx, old ? SD_LLDP_RX_EVENT_UPDATED : SD_LLDP_RX_EVENT_ADDED, n); + + return 1; + +finish: + if (old) + lldp_rx_callback(lldp_rx, SD_LLDP_RX_EVENT_REMOVED, old); + + return r; +} + +static int lldp_rx_handle_datagram(sd_lldp_rx *lldp_rx, sd_lldp_neighbor *n) { + int r; + + assert(lldp_rx); + assert(n); + + r = lldp_neighbor_parse(n); + if (r < 0) + return r; + + r = lldp_rx_add_neighbor(lldp_rx, n); + if (r < 0) + return log_lldp_rx_errno(lldp_rx, r, "Failed to add datagram. Ignoring."); + + log_lldp_rx(lldp_rx, "Successfully processed LLDP datagram."); + return 0; +} + +static int lldp_rx_receive_datagram(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL; + ssize_t space, length; + sd_lldp_rx *lldp_rx = ASSERT_PTR(userdata); + struct timespec ts; + + assert(fd >= 0); + + space = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(space) || ERRNO_IS_NEG_DISCONNECT(space)) + return 0; + if (space < 0) { + log_lldp_rx_errno(lldp_rx, space, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + n = lldp_neighbor_new(space); + if (!n) { + log_oom_debug(); + return 0; + } + + length = recv(fd, LLDP_NEIGHBOR_RAW(n), n->raw_size, MSG_DONTWAIT); + if (length < 0) { + if (ERRNO_IS_TRANSIENT(errno) || ERRNO_IS_DISCONNECT(errno)) + return 0; + + log_lldp_rx_errno(lldp_rx, errno, "Failed to read LLDP datagram, ignoring: %m"); + return 0; + } + + if ((size_t) length != n->raw_size) { + log_lldp_rx(lldp_rx, "Packet size mismatch, ignoring"); + return 0; + } + + /* Try to get the timestamp of this packet if it is known */ + if (ioctl(fd, SIOCGSTAMPNS, &ts) >= 0) + triple_timestamp_from_realtime(&n->timestamp, timespec_load(&ts)); + else + triple_timestamp_now(&n->timestamp); + + (void) lldp_rx_handle_datagram(lldp_rx, n); + return 0; +} + +static void lldp_rx_reset(sd_lldp_rx *lldp_rx) { + assert(lldp_rx); + + (void) event_source_disable(lldp_rx->timer_event_source); + lldp_rx->io_event_source = sd_event_source_disable_unref(lldp_rx->io_event_source); + lldp_rx->fd = safe_close(lldp_rx->fd); +} + +int sd_lldp_rx_is_running(sd_lldp_rx *lldp_rx) { + if (!lldp_rx) + return false; + + return lldp_rx->fd >= 0; +} + +int sd_lldp_rx_start(sd_lldp_rx *lldp_rx) { + int r; + + assert_return(lldp_rx, -EINVAL); + assert_return(lldp_rx->event, -EINVAL); + assert_return(lldp_rx->ifindex > 0, -EINVAL); + + if (sd_lldp_rx_is_running(lldp_rx)) + return 0; + + assert(!lldp_rx->io_event_source); + + lldp_rx->fd = lldp_network_bind_raw_socket(lldp_rx->ifindex); + if (lldp_rx->fd < 0) + return lldp_rx->fd; + + r = sd_event_add_io(lldp_rx->event, &lldp_rx->io_event_source, lldp_rx->fd, EPOLLIN, lldp_rx_receive_datagram, lldp_rx); + if (r < 0) + goto fail; + + r = sd_event_source_set_priority(lldp_rx->io_event_source, lldp_rx->event_priority); + if (r < 0) + goto fail; + + (void) sd_event_source_set_description(lldp_rx->io_event_source, "lldp-rx-io"); + + log_lldp_rx(lldp_rx, "Started LLDP client"); + return 1; + +fail: + lldp_rx_reset(lldp_rx); + return r; +} + +int sd_lldp_rx_stop(sd_lldp_rx *lldp_rx) { + if (!sd_lldp_rx_is_running(lldp_rx)) + return 0; + + log_lldp_rx(lldp_rx, "Stopping LLDP client"); + + lldp_rx_reset(lldp_rx); + lldp_rx_flush_neighbors(lldp_rx); + + return 1; +} + +int sd_lldp_rx_attach_event(sd_lldp_rx *lldp_rx, sd_event *event, int64_t priority) { + int r; + + assert_return(lldp_rx, -EINVAL); + assert_return(!sd_lldp_rx_is_running(lldp_rx), -EBUSY); + assert_return(!lldp_rx->event, -EBUSY); + + if (event) + lldp_rx->event = sd_event_ref(event); + else { + r = sd_event_default(&lldp_rx->event); + if (r < 0) + return r; + } + + lldp_rx->event_priority = priority; + + return 0; +} + +int sd_lldp_rx_detach_event(sd_lldp_rx *lldp_rx) { + assert_return(lldp_rx, -EINVAL); + assert_return(!sd_lldp_rx_is_running(lldp_rx), -EBUSY); + + lldp_rx->io_event_source = sd_event_source_disable_unref(lldp_rx->io_event_source); + lldp_rx->timer_event_source = sd_event_source_disable_unref(lldp_rx->timer_event_source); + lldp_rx->event = sd_event_unref(lldp_rx->event); + return 0; +} + +sd_event* sd_lldp_rx_get_event(sd_lldp_rx *lldp_rx) { + assert_return(lldp_rx, NULL); + + return lldp_rx->event; +} + +int sd_lldp_rx_set_callback(sd_lldp_rx *lldp_rx, sd_lldp_rx_callback_t cb, void *userdata) { + assert_return(lldp_rx, -EINVAL); + + lldp_rx->callback = cb; + lldp_rx->userdata = userdata; + + return 0; +} + +int sd_lldp_rx_set_ifindex(sd_lldp_rx *lldp_rx, int ifindex) { + assert_return(lldp_rx, -EINVAL); + assert_return(ifindex > 0, -EINVAL); + assert_return(!sd_lldp_rx_is_running(lldp_rx), -EBUSY); + + lldp_rx->ifindex = ifindex; + return 0; +} + +int sd_lldp_rx_set_ifname(sd_lldp_rx *lldp_rx, const char *ifname) { + assert_return(lldp_rx, -EINVAL); + assert_return(ifname, -EINVAL); + + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) + return -EINVAL; + + return free_and_strdup(&lldp_rx->ifname, ifname); +} + +int sd_lldp_rx_get_ifname(sd_lldp_rx *lldp_rx, const char **ret) { + int r; + + assert_return(lldp_rx, -EINVAL); + + r = get_ifname(lldp_rx->ifindex, &lldp_rx->ifname); + if (r < 0) + return r; + + if (ret) + *ret = lldp_rx->ifname; + + return 0; +} + +static sd_lldp_rx *lldp_rx_free(sd_lldp_rx *lldp_rx) { + if (!lldp_rx) + return NULL; + + lldp_rx_reset(lldp_rx); + + sd_lldp_rx_detach_event(lldp_rx); + + lldp_rx_flush_neighbors(lldp_rx); + + hashmap_free(lldp_rx->neighbor_by_id); + prioq_free(lldp_rx->neighbor_by_expiry); + free(lldp_rx->ifname); + return mfree(lldp_rx); +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_lldp_rx, sd_lldp_rx, lldp_rx_free); + +int sd_lldp_rx_new(sd_lldp_rx **ret) { + _cleanup_(sd_lldp_rx_unrefp) sd_lldp_rx *lldp_rx = NULL; + + assert_return(ret, -EINVAL); + + lldp_rx = new(sd_lldp_rx, 1); + if (!lldp_rx) + return -ENOMEM; + + *lldp_rx = (sd_lldp_rx) { + .n_ref = 1, + .fd = -EBADF, + .neighbors_max = LLDP_DEFAULT_NEIGHBORS_MAX, + .capability_mask = UINT16_MAX, + }; + + *ret = TAKE_PTR(lldp_rx); + return 0; +} + +static int on_timer_event(sd_event_source *s, uint64_t usec, void *userdata) { + sd_lldp_rx *lldp_rx = userdata; + int r; + + r = lldp_rx_make_space(lldp_rx, 0); + if (r < 0) { + log_lldp_rx_errno(lldp_rx, r, "Failed to make space, ignoring: %m"); + return 0; + } + + r = lldp_rx_start_timer(lldp_rx, NULL); + if (r < 0) { + log_lldp_rx_errno(lldp_rx, r, "Failed to restart timer, ignoring: %m"); + return 0; + } + + return 0; +} + +static int lldp_rx_start_timer(sd_lldp_rx *lldp_rx, sd_lldp_neighbor *neighbor) { + sd_lldp_neighbor *n; + + assert(lldp_rx); + assert(lldp_rx->event); + + if (neighbor) + lldp_neighbor_start_ttl(neighbor); + + n = prioq_peek(lldp_rx->neighbor_by_expiry); + if (!n) + return event_source_disable(lldp_rx->timer_event_source); + + return event_reset_time(lldp_rx->event, &lldp_rx->timer_event_source, + CLOCK_BOOTTIME, + n->until, 0, + on_timer_event, lldp_rx, + lldp_rx->event_priority, "lldp-rx-timer", true); +} + +static int neighbor_compare_func(sd_lldp_neighbor * const *a, sd_lldp_neighbor * const *b) { + assert(a); + assert(b); + assert(*a); + assert(*b); + + return lldp_neighbor_id_compare_func(&(*a)->id, &(*b)->id); +} + +int sd_lldp_rx_get_neighbors(sd_lldp_rx *lldp_rx, sd_lldp_neighbor ***ret) { + _cleanup_free_ sd_lldp_neighbor **l = NULL; + sd_lldp_neighbor *n; + int k = 0; + + assert_return(lldp_rx, -EINVAL); + assert_return(ret, -EINVAL); + + if (hashmap_isempty(lldp_rx->neighbor_by_id)) { /* Special shortcut */ + *ret = NULL; + return 0; + } + + l = new0(sd_lldp_neighbor*, hashmap_size(lldp_rx->neighbor_by_id)); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(n, lldp_rx->neighbor_by_id) + l[k++] = sd_lldp_neighbor_ref(n); + + assert((size_t) k == hashmap_size(lldp_rx->neighbor_by_id)); + + /* Return things in a stable order */ + typesafe_qsort(l, k, neighbor_compare_func); + *ret = TAKE_PTR(l); + + return k; +} + +int sd_lldp_rx_set_neighbors_max(sd_lldp_rx *lldp_rx, uint64_t m) { + assert_return(lldp_rx, -EINVAL); + assert_return(m > 0, -EINVAL); + + lldp_rx->neighbors_max = m; + lldp_rx_make_space(lldp_rx, 0); + + return 0; +} + +int sd_lldp_rx_match_capabilities(sd_lldp_rx *lldp_rx, uint16_t mask) { + assert_return(lldp_rx, -EINVAL); + assert_return(mask != 0, -EINVAL); + + lldp_rx->capability_mask = mask; + + return 0; +} + +int sd_lldp_rx_set_filter_address(sd_lldp_rx *lldp_rx, const struct ether_addr *addr) { + assert_return(lldp_rx, -EINVAL); + + /* In order to deal nicely with bridges that send back our own packets, allow one address to be filtered, so + * that our own can be filtered out here. */ + + if (addr) + lldp_rx->filter_address = *addr; + else + zero(lldp_rx->filter_address); + + return 0; +} diff --git a/src/libsystemd-network/sd-lldp-tx.c b/src/libsystemd-network/sd-lldp-tx.c new file mode 100644 index 0000000..2b822af --- /dev/null +++ b/src/libsystemd-network/sd-lldp-tx.c @@ -0,0 +1,628 @@ +/* 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; +} diff --git a/src/libsystemd-network/sd-ndisc.c b/src/libsystemd-network/sd-ndisc.c new file mode 100644 index 0000000..1beed5d --- /dev/null +++ b/src/libsystemd-network/sd-ndisc.c @@ -0,0 +1,381 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <netinet/icmp6.h> +#include <netinet/in.h> + +#include "sd-ndisc.h" + +#include "alloc-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "icmp6-util.h" +#include "in-addr-util.h" +#include "memory-util.h" +#include "ndisc-internal.h" +#include "ndisc-router.h" +#include "network-common.h" +#include "random-util.h" +#include "socket-util.h" +#include "string-table.h" +#include "string-util.h" + +#define NDISC_TIMEOUT_NO_RA_USEC (NDISC_ROUTER_SOLICITATION_INTERVAL * NDISC_MAX_ROUTER_SOLICITATIONS) + +static const char * const ndisc_event_table[_SD_NDISC_EVENT_MAX] = { + [SD_NDISC_EVENT_TIMEOUT] = "timeout", + [SD_NDISC_EVENT_ROUTER] = "router", +}; + +DEFINE_STRING_TABLE_LOOKUP(ndisc_event, sd_ndisc_event_t); + +static void ndisc_callback(sd_ndisc *ndisc, sd_ndisc_event_t event, sd_ndisc_router *rt) { + assert(ndisc); + assert(event >= 0 && event < _SD_NDISC_EVENT_MAX); + + if (!ndisc->callback) + return (void) log_ndisc(ndisc, "Received '%s' event.", ndisc_event_to_string(event)); + + log_ndisc(ndisc, "Invoking callback for '%s' event.", ndisc_event_to_string(event)); + ndisc->callback(ndisc, event, rt, ndisc->userdata); +} + +int sd_ndisc_set_callback( + sd_ndisc *nd, + sd_ndisc_callback_t callback, + void *userdata) { + + assert_return(nd, -EINVAL); + + nd->callback = callback; + nd->userdata = userdata; + + return 0; +} + +int sd_ndisc_set_ifindex(sd_ndisc *nd, int ifindex) { + assert_return(nd, -EINVAL); + assert_return(ifindex > 0, -EINVAL); + assert_return(nd->fd < 0, -EBUSY); + + nd->ifindex = ifindex; + return 0; +} + +int sd_ndisc_set_ifname(sd_ndisc *nd, const char *ifname) { + assert_return(nd, -EINVAL); + assert_return(ifname, -EINVAL); + + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) + return -EINVAL; + + return free_and_strdup(&nd->ifname, ifname); +} + +int sd_ndisc_get_ifname(sd_ndisc *nd, const char **ret) { + int r; + + assert_return(nd, -EINVAL); + + r = get_ifname(nd->ifindex, &nd->ifname); + if (r < 0) + return r; + + if (ret) + *ret = nd->ifname; + + return 0; +} + +int sd_ndisc_set_mac(sd_ndisc *nd, const struct ether_addr *mac_addr) { + assert_return(nd, -EINVAL); + + if (mac_addr) + nd->mac_addr = *mac_addr; + else + zero(nd->mac_addr); + + return 0; +} + +int sd_ndisc_attach_event(sd_ndisc *nd, sd_event *event, int64_t priority) { + int r; + + assert_return(nd, -EINVAL); + assert_return(nd->fd < 0, -EBUSY); + assert_return(!nd->event, -EBUSY); + + if (event) + nd->event = sd_event_ref(event); + else { + r = sd_event_default(&nd->event); + if (r < 0) + return 0; + } + + nd->event_priority = priority; + + return 0; +} + +int sd_ndisc_detach_event(sd_ndisc *nd) { + + assert_return(nd, -EINVAL); + assert_return(nd->fd < 0, -EBUSY); + + nd->event = sd_event_unref(nd->event); + return 0; +} + +sd_event *sd_ndisc_get_event(sd_ndisc *nd) { + assert_return(nd, NULL); + + return nd->event; +} + +static void ndisc_reset(sd_ndisc *nd) { + assert(nd); + + (void) event_source_disable(nd->timeout_event_source); + (void) event_source_disable(nd->timeout_no_ra); + nd->retransmit_time = 0; + nd->recv_event_source = sd_event_source_disable_unref(nd->recv_event_source); + nd->fd = safe_close(nd->fd); +} + +static sd_ndisc *ndisc_free(sd_ndisc *nd) { + assert(nd); + + ndisc_reset(nd); + + sd_event_source_unref(nd->timeout_event_source); + sd_event_source_unref(nd->timeout_no_ra); + sd_ndisc_detach_event(nd); + + free(nd->ifname); + return mfree(nd); +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_ndisc, sd_ndisc, ndisc_free); + +int sd_ndisc_new(sd_ndisc **ret) { + _cleanup_(sd_ndisc_unrefp) sd_ndisc *nd = NULL; + + assert_return(ret, -EINVAL); + + nd = new(sd_ndisc, 1); + if (!nd) + return -ENOMEM; + + *nd = (sd_ndisc) { + .n_ref = 1, + .fd = -EBADF, + }; + + *ret = TAKE_PTR(nd); + + return 0; +} + +static int ndisc_handle_datagram(sd_ndisc *nd, sd_ndisc_router *rt) { + int r; + + assert(nd); + assert(rt); + + r = ndisc_router_parse(nd, rt); + if (r < 0) + return r; + + log_ndisc(nd, "Received Router Advertisement: flags %s preference %s lifetime %s", + rt->flags & ND_RA_FLAG_MANAGED ? "MANAGED" : rt->flags & ND_RA_FLAG_OTHER ? "OTHER" : "none", + rt->preference == SD_NDISC_PREFERENCE_HIGH ? "high" : rt->preference == SD_NDISC_PREFERENCE_LOW ? "low" : "medium", + FORMAT_TIMESPAN(rt->lifetime_usec, USEC_PER_SEC)); + + ndisc_callback(nd, SD_NDISC_EVENT_ROUTER, rt); + return 0; +} + +static int ndisc_recv(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(sd_ndisc_router_unrefp) sd_ndisc_router *rt = NULL; + sd_ndisc *nd = ASSERT_PTR(userdata); + ssize_t buflen; + int r; + + assert(s); + assert(nd->event); + + buflen = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(buflen) || ERRNO_IS_NEG_DISCONNECT(buflen)) + return 0; + if (buflen < 0) { + log_ndisc_errno(nd, buflen, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + rt = ndisc_router_new(buflen); + if (!rt) + return -ENOMEM; + + r = icmp6_receive(fd, NDISC_ROUTER_RAW(rt), rt->raw_size, &rt->address, &rt->timestamp); + if (ERRNO_IS_NEG_TRANSIENT(r) || ERRNO_IS_NEG_DISCONNECT(r)) + return 0; + if (r < 0) + switch (r) { + case -EADDRNOTAVAIL: + log_ndisc(nd, "Received RA from neither link-local nor null address. Ignoring."); + return 0; + + case -EMULTIHOP: + log_ndisc(nd, "Received RA with invalid hop limit. Ignoring."); + return 0; + + case -EPFNOSUPPORT: + log_ndisc(nd, "Received invalid source address from ICMPv6 socket. Ignoring."); + return 0; + + default: + log_ndisc_errno(nd, r, "Unexpected error while reading from ICMPv6, ignoring: %m"); + return 0; + } + + /* The function icmp6_receive() accepts the null source address, but RFC 4861 Section 6.1.2 states + * that hosts MUST discard messages with the null source address. */ + if (in6_addr_is_null(&rt->address)) + log_ndisc(nd, "Received RA from null address. Ignoring."); + + (void) event_source_disable(nd->timeout_event_source); + (void) ndisc_handle_datagram(nd, rt); + return 0; +} + +static usec_t ndisc_timeout_compute_random(usec_t val) { + /* compute a time that is random within ±10% of the given value */ + return val - val / 10 + + (random_u64() % (2 * USEC_PER_SEC)) * val / 10 / USEC_PER_SEC; +} + +static int ndisc_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + sd_ndisc *nd = ASSERT_PTR(userdata); + usec_t time_now; + int r; + + assert(s); + assert(nd->event); + + assert_se(sd_event_now(nd->event, CLOCK_BOOTTIME, &time_now) >= 0); + + if (!nd->retransmit_time) + nd->retransmit_time = ndisc_timeout_compute_random(NDISC_ROUTER_SOLICITATION_INTERVAL); + else { + if (nd->retransmit_time > NDISC_MAX_ROUTER_SOLICITATION_INTERVAL / 2) + nd->retransmit_time = ndisc_timeout_compute_random(NDISC_MAX_ROUTER_SOLICITATION_INTERVAL); + else + nd->retransmit_time += ndisc_timeout_compute_random(nd->retransmit_time); + } + + r = event_reset_time(nd->event, &nd->timeout_event_source, + CLOCK_BOOTTIME, + time_now + nd->retransmit_time, 10 * USEC_PER_MSEC, + ndisc_timeout, nd, + nd->event_priority, "ndisc-timeout-no-ra", true); + if (r < 0) + goto fail; + + r = icmp6_send_router_solicitation(nd->fd, &nd->mac_addr); + if (r < 0) + log_ndisc_errno(nd, r, "Failed to send Router Solicitation, next solicitation in %s, ignoring: %m", + FORMAT_TIMESPAN(nd->retransmit_time, USEC_PER_SEC)); + else + log_ndisc(nd, "Sent Router Solicitation, next solicitation in %s", + FORMAT_TIMESPAN(nd->retransmit_time, USEC_PER_SEC)); + + return 0; + +fail: + (void) sd_ndisc_stop(nd); + return 0; +} + +static int ndisc_timeout_no_ra(sd_event_source *s, uint64_t usec, void *userdata) { + sd_ndisc *nd = ASSERT_PTR(userdata); + + assert(s); + + log_ndisc(nd, "No RA received before link confirmation timeout"); + + (void) event_source_disable(nd->timeout_no_ra); + ndisc_callback(nd, SD_NDISC_EVENT_TIMEOUT, NULL); + + return 0; +} + +int sd_ndisc_stop(sd_ndisc *nd) { + if (!nd) + return 0; + + if (nd->fd < 0) + return 0; + + log_ndisc(nd, "Stopping IPv6 Router Solicitation client"); + + ndisc_reset(nd); + return 1; +} + +int sd_ndisc_start(sd_ndisc *nd) { + int r; + usec_t time_now; + + assert_return(nd, -EINVAL); + assert_return(nd->event, -EINVAL); + assert_return(nd->ifindex > 0, -EINVAL); + + if (nd->fd >= 0) + return 0; + + assert(!nd->recv_event_source); + + r = sd_event_now(nd->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + goto fail; + + nd->fd = icmp6_bind_router_solicitation(nd->ifindex); + if (nd->fd < 0) + return nd->fd; + + r = sd_event_add_io(nd->event, &nd->recv_event_source, nd->fd, EPOLLIN, ndisc_recv, nd); + if (r < 0) + goto fail; + + r = sd_event_source_set_priority(nd->recv_event_source, nd->event_priority); + if (r < 0) + goto fail; + + (void) sd_event_source_set_description(nd->recv_event_source, "ndisc-receive-message"); + + r = event_reset_time(nd->event, &nd->timeout_event_source, + CLOCK_BOOTTIME, + time_now + USEC_PER_SEC / 2, 1 * USEC_PER_SEC, /* See RFC 8415 sec. 18.2.1 */ + ndisc_timeout, nd, + nd->event_priority, "ndisc-timeout", true); + if (r < 0) + goto fail; + + r = event_reset_time(nd->event, &nd->timeout_no_ra, + CLOCK_BOOTTIME, + time_now + NDISC_TIMEOUT_NO_RA_USEC, 10 * USEC_PER_MSEC, + ndisc_timeout_no_ra, nd, + nd->event_priority, "ndisc-timeout-no-ra", true); + if (r < 0) + goto fail; + + log_ndisc(nd, "Started IPv6 Router Solicitation client"); + return 1; + +fail: + ndisc_reset(nd); + return r; +} diff --git a/src/libsystemd-network/sd-radv.c b/src/libsystemd-network/sd-radv.c new file mode 100644 index 0000000..97d306c --- /dev/null +++ b/src/libsystemd-network/sd-radv.c @@ -0,0 +1,1161 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2017 Intel Corporation. All rights reserved. +***/ + +#include <netinet/icmp6.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include "sd-radv.h" + +#include "alloc-util.h" +#include "dns-domain.h" +#include "ether-addr-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "icmp6-util.h" +#include "in-addr-util.h" +#include "iovec-util.h" +#include "macro.h" +#include "memory-util.h" +#include "network-common.h" +#include "radv-internal.h" +#include "random-util.h" +#include "socket-util.h" +#include "string-util.h" +#include "strv.h" +#include "unaligned.h" + +int sd_radv_new(sd_radv **ret) { + _cleanup_(sd_radv_unrefp) sd_radv *ra = NULL; + + assert_return(ret, -EINVAL); + + ra = new(sd_radv, 1); + if (!ra) + return -ENOMEM; + + *ra = (sd_radv) { + .n_ref = 1, + .fd = -EBADF, + .lifetime_usec = RADV_DEFAULT_ROUTER_LIFETIME_USEC, + }; + + *ret = TAKE_PTR(ra); + + return 0; +} + +int sd_radv_attach_event(sd_radv *ra, sd_event *event, int64_t priority) { + int r; + + assert_return(ra, -EINVAL); + assert_return(!ra->event, -EBUSY); + + if (event) + ra->event = sd_event_ref(event); + else { + r = sd_event_default(&ra->event); + if (r < 0) + return 0; + } + + ra->event_priority = priority; + + return 0; +} + +int sd_radv_detach_event(sd_radv *ra) { + + assert_return(ra, -EINVAL); + + ra->event = sd_event_unref(ra->event); + return 0; +} + +sd_event *sd_radv_get_event(sd_radv *ra) { + assert_return(ra, NULL); + + return ra->event; +} + +int sd_radv_is_running(sd_radv *ra) { + assert_return(ra, false); + + return ra->state != RADV_STATE_IDLE; +} + +static void radv_reset(sd_radv *ra) { + assert(ra); + + (void) event_source_disable(ra->timeout_event_source); + + ra->recv_event_source = sd_event_source_disable_unref(ra->recv_event_source); + + ra->ra_sent = 0; +} + +static sd_radv *radv_free(sd_radv *ra) { + if (!ra) + return NULL; + + LIST_CLEAR(prefix, ra->prefixes, sd_radv_prefix_unref); + LIST_CLEAR(prefix, ra->route_prefixes, sd_radv_route_prefix_unref); + LIST_CLEAR(prefix, ra->pref64_prefixes, sd_radv_pref64_prefix_unref); + + free(ra->rdnss); + free(ra->dnssl); + + radv_reset(ra); + + sd_event_source_unref(ra->timeout_event_source); + sd_radv_detach_event(ra); + + ra->fd = safe_close(ra->fd); + free(ra->ifname); + + return mfree(ra); +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_radv, sd_radv, radv_free); + +static bool router_lifetime_is_valid(usec_t lifetime_usec) { + return lifetime_usec == 0 || + (lifetime_usec >= RADV_MIN_ROUTER_LIFETIME_USEC && + lifetime_usec <= RADV_MAX_ROUTER_LIFETIME_USEC); +} + +static int radv_send(sd_radv *ra, const struct in6_addr *dst, usec_t lifetime_usec) { + struct sockaddr_in6 dst_addr = { + .sin6_family = AF_INET6, + .sin6_addr = IN6ADDR_ALL_NODES_MULTICAST_INIT, + }; + struct nd_router_advert adv = {}; + struct { + struct nd_opt_hdr opthdr; + struct ether_addr slladdr; + } _packed_ opt_mac = { + .opthdr = { + .nd_opt_type = ND_OPT_SOURCE_LINKADDR, + .nd_opt_len = (sizeof(struct nd_opt_hdr) + + sizeof(struct ether_addr) - 1) /8 + 1, + }, + }; + struct nd_opt_mtu opt_mtu = { + .nd_opt_mtu_type = ND_OPT_MTU, + .nd_opt_mtu_len = 1, + }; + /* Reserve iov space for RA header, linkaddr, MTU, N prefixes, N routes, N pref64 prefixes, RDNSS, + * DNSSL, and home agent. */ + struct iovec iov[6 + ra->n_prefixes + ra->n_route_prefixes + ra->n_pref64_prefixes]; + struct msghdr msg = { + .msg_name = &dst_addr, + .msg_namelen = sizeof(dst_addr), + .msg_iov = iov, + }; + usec_t time_now; + int r; + + assert(ra); + assert(router_lifetime_is_valid(lifetime_usec)); + + r = sd_event_now(ra->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + return r; + + if (dst && in6_addr_is_set(dst)) + dst_addr.sin6_addr = *dst; + + adv.nd_ra_type = ND_ROUTER_ADVERT; + adv.nd_ra_curhoplimit = ra->hop_limit; + adv.nd_ra_retransmit = usec_to_be32_msec(ra->retransmit_usec); + adv.nd_ra_flags_reserved = ra->flags; + assert_cc(RADV_MAX_ROUTER_LIFETIME_USEC <= UINT16_MAX * USEC_PER_SEC); + adv.nd_ra_router_lifetime = usec_to_be16_sec(lifetime_usec); + iov[msg.msg_iovlen++] = IOVEC_MAKE(&adv, sizeof(adv)); + + /* MAC address is optional, either because the link does not use L2 + addresses or load sharing is desired. See RFC 4861, Section 4.2 */ + if (!ether_addr_is_null(&ra->mac_addr)) { + opt_mac.slladdr = ra->mac_addr; + iov[msg.msg_iovlen++] = IOVEC_MAKE(&opt_mac, sizeof(opt_mac)); + } + + if (ra->mtu > 0) { + opt_mtu.nd_opt_mtu_mtu = htobe32(ra->mtu); + iov[msg.msg_iovlen++] = IOVEC_MAKE(&opt_mtu, sizeof(opt_mtu)); + } + + LIST_FOREACH(prefix, p, ra->prefixes) { + usec_t lifetime_valid_usec, lifetime_preferred_usec; + + lifetime_valid_usec = MIN(usec_sub_unsigned(p->valid_until, time_now), + p->lifetime_valid_usec); + + lifetime_preferred_usec = MIN3(usec_sub_unsigned(p->preferred_until, time_now), + p->lifetime_preferred_usec, + lifetime_valid_usec); + + p->opt.lifetime_valid = usec_to_be32_sec(lifetime_valid_usec); + p->opt.lifetime_preferred = usec_to_be32_sec(lifetime_preferred_usec); + + iov[msg.msg_iovlen++] = IOVEC_MAKE(&p->opt, sizeof(p->opt)); + } + + LIST_FOREACH(prefix, rt, ra->route_prefixes) { + rt->opt.lifetime = usec_to_be32_sec(MIN(usec_sub_unsigned(rt->valid_until, time_now), + rt->lifetime_usec)); + + iov[msg.msg_iovlen++] = IOVEC_MAKE(&rt->opt, sizeof(rt->opt)); + } + + LIST_FOREACH(prefix, p, ra->pref64_prefixes) + iov[msg.msg_iovlen++] = IOVEC_MAKE(&p->opt, sizeof(p->opt)); + + if (ra->rdnss) + iov[msg.msg_iovlen++] = IOVEC_MAKE(ra->rdnss, ra->rdnss->length * 8); + + if (ra->dnssl) + iov[msg.msg_iovlen++] = IOVEC_MAKE(ra->dnssl, ra->dnssl->length * 8); + + if (FLAGS_SET(ra->flags, ND_RA_FLAG_HOME_AGENT)) { + ra->home_agent.nd_opt_home_agent_info_type = ND_OPT_HOME_AGENT_INFO; + ra->home_agent.nd_opt_home_agent_info_len = 1; + + /* 0 means to place the current Router Lifetime value */ + if (ra->home_agent.nd_opt_home_agent_info_lifetime == 0) + ra->home_agent.nd_opt_home_agent_info_lifetime = adv.nd_ra_router_lifetime; + + iov[msg.msg_iovlen++] = IOVEC_MAKE(&ra->home_agent, sizeof(ra->home_agent)); + } + + if (sendmsg(ra->fd, &msg, 0) < 0) + return -errno; + + return 0; +} + +static int radv_recv(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + sd_radv *ra = ASSERT_PTR(userdata); + struct in6_addr src; + triple_timestamp timestamp; + int r; + + assert(s); + assert(ra->event); + + ssize_t buflen = next_datagram_size_fd(fd); + if (ERRNO_IS_NEG_TRANSIENT(buflen) || ERRNO_IS_NEG_DISCONNECT(buflen)) + return 0; + if (buflen < 0) { + log_radv_errno(ra, buflen, "Failed to determine datagram size to read, ignoring: %m"); + return 0; + } + + _cleanup_free_ char *buf = new0(char, buflen); + if (!buf) + return -ENOMEM; + + r = icmp6_receive(fd, buf, buflen, &src, ×tamp); + if (ERRNO_IS_NEG_TRANSIENT(r) || ERRNO_IS_NEG_DISCONNECT(r)) + return 0; + if (r < 0) + switch (r) { + case -EADDRNOTAVAIL: + log_radv(ra, "Received RS from neither link-local nor null address. Ignoring"); + return 0; + + case -EMULTIHOP: + log_radv(ra, "Received RS with invalid hop limit. Ignoring."); + return 0; + + case -EPFNOSUPPORT: + log_radv(ra, "Received invalid source address from ICMPv6 socket. Ignoring."); + return 0; + + default: + log_radv_errno(ra, r, "Unexpected error receiving from ICMPv6 socket, ignoring: %m"); + return 0; + } + + if ((size_t) buflen < sizeof(struct nd_router_solicit)) { + log_radv(ra, "Too short packet received, ignoring"); + return 0; + } + + /* TODO: if the sender address is null, check that the message does not have the source link-layer + * address option. See RFC 4861 Section 6.1.1. */ + + const char *addr = IN6_ADDR_TO_STRING(&src); + + r = radv_send(ra, &src, ra->lifetime_usec); + if (r < 0) + log_radv_errno(ra, r, "Unable to send solicited Router Advertisement to %s, ignoring: %m", addr); + else + log_radv(ra, "Sent solicited Router Advertisement to %s", addr); + + return 0; +} + +static int radv_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + usec_t min_timeout, max_timeout, time_now, timeout; + sd_radv *ra = ASSERT_PTR(userdata); + int r; + + assert(s); + assert(ra->event); + assert(router_lifetime_is_valid(ra->lifetime_usec)); + + r = sd_event_now(ra->event, CLOCK_BOOTTIME, &time_now); + if (r < 0) + goto fail; + + r = radv_send(ra, NULL, ra->lifetime_usec); + if (r < 0) + log_radv_errno(ra, r, "Unable to send Router Advertisement, ignoring: %m"); + + /* RFC 4861, Section 6.2.4, sending initial Router Advertisements */ + if (ra->ra_sent < RADV_MAX_INITIAL_RTR_ADVERTISEMENTS) + max_timeout = RADV_MAX_INITIAL_RTR_ADVERT_INTERVAL_USEC; + else + max_timeout = RADV_DEFAULT_MAX_TIMEOUT_USEC; + + /* RFC 4861, Section 6.2.1, lifetime must be at least MaxRtrAdvInterval, + * so lower the interval here */ + if (ra->lifetime_usec > 0) + max_timeout = MIN(max_timeout, ra->lifetime_usec); + + if (max_timeout >= 9 * USEC_PER_SEC) + min_timeout = max_timeout / 3; + else + min_timeout = max_timeout * 3 / 4; + + /* RFC 4861, Section 6.2.1. + * MaxRtrAdvInterval MUST be no less than 4 seconds and no greater than 1800 seconds. + * MinRtrAdvInterval MUST be no less than 3 seconds and no greater than .75 * MaxRtrAdvInterval. */ + assert(max_timeout >= RADV_MIN_MAX_TIMEOUT_USEC); + assert(max_timeout <= RADV_MAX_MAX_TIMEOUT_USEC); + assert(min_timeout >= RADV_MIN_MIN_TIMEOUT_USEC); + assert(min_timeout <= max_timeout * 3 / 4); + + timeout = min_timeout + random_u64_range(max_timeout - min_timeout); + log_radv(ra, "Next Router Advertisement in %s", FORMAT_TIMESPAN(timeout, USEC_PER_SEC)); + + r = event_reset_time(ra->event, &ra->timeout_event_source, + CLOCK_BOOTTIME, + usec_add(time_now, timeout), MSEC_PER_SEC, + radv_timeout, ra, + ra->event_priority, "radv-timeout", true); + if (r < 0) + goto fail; + + ra->ra_sent++; + + return 0; + +fail: + sd_radv_stop(ra); + + return 0; +} + +int sd_radv_stop(sd_radv *ra) { + int r; + + if (!ra) + return 0; + + if (ra->state == RADV_STATE_IDLE) + return 0; + + log_radv(ra, "Stopping IPv6 Router Advertisement daemon"); + + /* RFC 4861, Section 6.2.5, send at least one Router Advertisement + with zero lifetime */ + r = radv_send(ra, NULL, 0); + if (r < 0) + log_radv_errno(ra, r, "Unable to send last Router Advertisement with router lifetime set to zero, ignoring: %m"); + + radv_reset(ra); + ra->fd = safe_close(ra->fd); + ra->state = RADV_STATE_IDLE; + + return 0; +} + +int sd_radv_start(sd_radv *ra) { + int r; + + assert_return(ra, -EINVAL); + assert_return(ra->event, -EINVAL); + assert_return(ra->ifindex > 0, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return 0; + + r = event_reset_time(ra->event, &ra->timeout_event_source, + CLOCK_BOOTTIME, + 0, 0, + radv_timeout, ra, + ra->event_priority, "radv-timeout", true); + if (r < 0) + goto fail; + + r = icmp6_bind_router_advertisement(ra->ifindex); + if (r < 0) + goto fail; + + ra->fd = r; + + r = sd_event_add_io(ra->event, &ra->recv_event_source, ra->fd, EPOLLIN, radv_recv, ra); + if (r < 0) + goto fail; + + r = sd_event_source_set_priority(ra->recv_event_source, ra->event_priority); + if (r < 0) + goto fail; + + (void) sd_event_source_set_description(ra->recv_event_source, "radv-receive-message"); + + ra->state = RADV_STATE_ADVERTISING; + + log_radv(ra, "Started IPv6 Router Advertisement daemon"); + + return 0; + + fail: + radv_reset(ra); + + return r; +} + +int sd_radv_set_ifindex(sd_radv *ra, int ifindex) { + assert_return(ra, -EINVAL); + assert_return(ifindex > 0, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + ra->ifindex = ifindex; + + return 0; +} + +int sd_radv_set_ifname(sd_radv *ra, const char *ifname) { + assert_return(ra, -EINVAL); + assert_return(ifname, -EINVAL); + + if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE)) + return -EINVAL; + + return free_and_strdup(&ra->ifname, ifname); +} + +int sd_radv_get_ifname(sd_radv *ra, const char **ret) { + int r; + + assert_return(ra, -EINVAL); + + r = get_ifname(ra->ifindex, &ra->ifname); + if (r < 0) + return r; + + if (ret) + *ret = ra->ifname; + + return 0; +} + +int sd_radv_set_mac(sd_radv *ra, const struct ether_addr *mac_addr) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + if (mac_addr) + ra->mac_addr = *mac_addr; + else + zero(ra->mac_addr); + + return 0; +} + +int sd_radv_set_mtu(sd_radv *ra, uint32_t mtu) { + assert_return(ra, -EINVAL); + assert_return(mtu >= 1280, -EINVAL); + + ra->mtu = mtu; + + return 0; +} + +int sd_radv_set_hop_limit(sd_radv *ra, uint8_t hop_limit) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + ra->hop_limit = hop_limit; + + return 0; +} + +int sd_radv_set_retransmit(sd_radv *ra, uint64_t usec) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + if (usec > RADV_MAX_RETRANSMIT_USEC) + return -EINVAL; + + ra->retransmit_usec = usec; + return 0; +} + +int sd_radv_set_router_lifetime(sd_radv *ra, uint64_t usec) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + if (!router_lifetime_is_valid(usec)) + return -EINVAL; + + /* RFC 4191, Section 2.2, "...If the Router Lifetime is zero, the preference value MUST be set + * to (00) by the sender..." */ + if (usec == 0 && + (ra->flags & (0x3 << 3)) != (SD_NDISC_PREFERENCE_MEDIUM << 3)) + return -EINVAL; + + ra->lifetime_usec = usec; + return 0; +} + +int sd_radv_set_managed_information(sd_radv *ra, int managed) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + SET_FLAG(ra->flags, ND_RA_FLAG_MANAGED, managed); + + return 0; +} + +int sd_radv_set_other_information(sd_radv *ra, int other) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + SET_FLAG(ra->flags, ND_RA_FLAG_OTHER, other); + + return 0; +} + +int sd_radv_set_preference(sd_radv *ra, unsigned preference) { + assert_return(ra, -EINVAL); + assert_return(IN_SET(preference, + SD_NDISC_PREFERENCE_LOW, + SD_NDISC_PREFERENCE_MEDIUM, + SD_NDISC_PREFERENCE_HIGH), -EINVAL); + + /* RFC 4191, Section 2.2, "...If the Router Lifetime is zero, the preference value MUST be set + * to (00) by the sender..." */ + if (ra->lifetime_usec == 0 && preference != SD_NDISC_PREFERENCE_MEDIUM) + return -EINVAL; + + ra->flags = (ra->flags & ~(0x3 << 3)) | (preference << 3); + + return 0; +} + +int sd_radv_set_home_agent_information(sd_radv *ra, int home_agent) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + SET_FLAG(ra->flags, ND_RA_FLAG_HOME_AGENT, home_agent); + + return 0; +} + +int sd_radv_set_home_agent_preference(sd_radv *ra, uint16_t preference) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + ra->home_agent.nd_opt_home_agent_info_preference = htobe16(preference); + + return 0; +} + +int sd_radv_set_home_agent_lifetime(sd_radv *ra, uint64_t lifetime_usec) { + assert_return(ra, -EINVAL); + + if (ra->state != RADV_STATE_IDLE) + return -EBUSY; + + if (lifetime_usec > RADV_HOME_AGENT_MAX_LIFETIME_USEC) + return -EINVAL; + + ra->home_agent.nd_opt_home_agent_info_lifetime = usec_to_be16_sec(lifetime_usec); + return 0; +} + +int sd_radv_add_prefix(sd_radv *ra, sd_radv_prefix *p) { + sd_radv_prefix *found = NULL; + int r; + + assert_return(ra, -EINVAL); + assert_return(p, -EINVAL); + + /* Refuse prefixes that don't have a prefix set */ + if (in6_addr_is_null(&p->opt.in6_addr)) + return -ENOEXEC; + + const char *addr_p = IN6_ADDR_PREFIX_TO_STRING(&p->opt.in6_addr, p->opt.prefixlen); + + LIST_FOREACH(prefix, cur, ra->prefixes) { + r = in_addr_prefix_intersect(AF_INET6, + (const union in_addr_union*) &cur->opt.in6_addr, + cur->opt.prefixlen, + (const union in_addr_union*) &p->opt.in6_addr, + p->opt.prefixlen); + if (r < 0) + return r; + if (r == 0) + continue; + + if (cur->opt.prefixlen == p->opt.prefixlen) { + found = cur; + break; + } + + return log_radv_errno(ra, SYNTHETIC_ERRNO(EEXIST), + "IPv6 prefix %s conflicts with %s, ignoring.", + addr_p, + IN6_ADDR_PREFIX_TO_STRING(&cur->opt.in6_addr, cur->opt.prefixlen)); + } + + if (found) { + /* p and cur may be equivalent. First increment the reference counter. */ + sd_radv_prefix_ref(p); + + /* Then, remove the old entry. */ + LIST_REMOVE(prefix, ra->prefixes, found); + sd_radv_prefix_unref(found); + + /* Finally, add the new entry. */ + LIST_APPEND(prefix, ra->prefixes, p); + + log_radv(ra, "Updated/replaced IPv6 prefix %s (preferred: %s, valid: %s)", + addr_p, + FORMAT_TIMESPAN(p->lifetime_preferred_usec, USEC_PER_SEC), + FORMAT_TIMESPAN(p->lifetime_valid_usec, USEC_PER_SEC)); + } else { + /* The prefix is new. Let's simply add it. */ + + sd_radv_prefix_ref(p); + LIST_APPEND(prefix, ra->prefixes, p); + ra->n_prefixes++; + + log_radv(ra, "Added prefix %s", addr_p); + } + + if (ra->state == RADV_STATE_IDLE) + return 0; + + if (ra->ra_sent == 0) + return 0; + + /* If RAs have already been sent, send an RA immediately to announce the newly-added prefix */ + r = radv_send(ra, NULL, ra->lifetime_usec); + if (r < 0) + log_radv_errno(ra, r, "Unable to send Router Advertisement for added prefix %s, ignoring: %m", addr_p); + else + log_radv(ra, "Sent Router Advertisement for added/updated prefix %s.", addr_p); + + return 0; +} + +void sd_radv_remove_prefix( + sd_radv *ra, + const struct in6_addr *prefix, + unsigned char prefixlen) { + + if (!ra) + return; + + if (!prefix) + return; + + LIST_FOREACH(prefix, cur, ra->prefixes) { + if (prefixlen != cur->opt.prefixlen) + continue; + + if (!in6_addr_equal(prefix, &cur->opt.in6_addr)) + continue; + + LIST_REMOVE(prefix, ra->prefixes, cur); + ra->n_prefixes--; + sd_radv_prefix_unref(cur); + return; + } +} + +int sd_radv_add_route_prefix(sd_radv *ra, sd_radv_route_prefix *p) { + sd_radv_route_prefix *found = NULL; + int r; + + assert_return(ra, -EINVAL); + assert_return(p, -EINVAL); + + const char *addr_p = IN6_ADDR_PREFIX_TO_STRING(&p->opt.in6_addr, p->opt.prefixlen); + + LIST_FOREACH(prefix, cur, ra->route_prefixes) { + r = in_addr_prefix_intersect(AF_INET6, + (const union in_addr_union*) &cur->opt.in6_addr, + cur->opt.prefixlen, + (const union in_addr_union*) &p->opt.in6_addr, + p->opt.prefixlen); + if (r < 0) + return r; + if (r == 0) + continue; + + if (cur->opt.prefixlen == p->opt.prefixlen) { + found = cur; + break; + } + + return log_radv_errno(ra, SYNTHETIC_ERRNO(EEXIST), + "IPv6 route prefix %s conflicts with %s, ignoring.", + addr_p, + IN6_ADDR_PREFIX_TO_STRING(&cur->opt.in6_addr, cur->opt.prefixlen)); + } + + if (found) { + /* p and cur may be equivalent. First increment the reference counter. */ + sd_radv_route_prefix_ref(p); + + /* Then, remove the old entry. */ + LIST_REMOVE(prefix, ra->route_prefixes, found); + sd_radv_route_prefix_unref(found); + + /* Finally, add the new entry. */ + LIST_APPEND(prefix, ra->route_prefixes, p); + + log_radv(ra, "Updated/replaced IPv6 route prefix %s (lifetime: %s)", + strna(addr_p), + FORMAT_TIMESPAN(p->lifetime_usec, USEC_PER_SEC)); + } else { + /* The route prefix is new. Let's simply add it. */ + + sd_radv_route_prefix_ref(p); + LIST_APPEND(prefix, ra->route_prefixes, p); + ra->n_route_prefixes++; + + log_radv(ra, "Added route prefix %s", strna(addr_p)); + } + + if (ra->state == RADV_STATE_IDLE) + return 0; + + if (ra->ra_sent == 0) + return 0; + + /* If RAs have already been sent, send an RA immediately to announce the newly-added route prefix */ + r = radv_send(ra, NULL, ra->lifetime_usec); + if (r < 0) + log_radv_errno(ra, r, "Unable to send Router Advertisement for added route prefix %s, ignoring: %m", + strna(addr_p)); + else + log_radv(ra, "Sent Router Advertisement for added route prefix %s.", strna(addr_p)); + + return 0; +} + +int sd_radv_add_pref64_prefix(sd_radv *ra, sd_radv_pref64_prefix *p) { + sd_radv_pref64_prefix *found = NULL; + int r; + + assert_return(ra, -EINVAL); + assert_return(p, -EINVAL); + + const char *addr_p = IN6_ADDR_PREFIX_TO_STRING(&p->in6_addr, p->prefixlen); + + LIST_FOREACH(prefix, cur, ra->pref64_prefixes) { + r = in_addr_prefix_intersect(AF_INET6, + (const union in_addr_union*) &cur->in6_addr, + cur->prefixlen, + (const union in_addr_union*) &p->in6_addr, + p->prefixlen); + if (r < 0) + return r; + if (r == 0) + continue; + + if (cur->prefixlen == p->prefixlen) { + found = cur; + break; + } + + return log_radv_errno(ra, SYNTHETIC_ERRNO(EEXIST), + "IPv6 PREF64 prefix %s conflicts with %s, ignoring.", + addr_p, + IN6_ADDR_PREFIX_TO_STRING(&cur->in6_addr, cur->prefixlen)); + } + + if (found) { + /* p and cur may be equivalent. First increment the reference counter. */ + sd_radv_pref64_prefix_ref(p); + + /* Then, remove the old entry. */ + LIST_REMOVE(prefix, ra->pref64_prefixes, found); + sd_radv_pref64_prefix_unref(found); + + /* Finally, add the new entry. */ + LIST_APPEND(prefix, ra->pref64_prefixes, p); + + log_radv(ra, "Updated/replaced IPv6 PREF64 prefix %s (lifetime: %s)", + strna(addr_p), + FORMAT_TIMESPAN(p->lifetime_usec, USEC_PER_SEC)); + } else { + /* The route prefix is new. Let's simply add it. */ + + sd_radv_pref64_prefix_ref(p); + LIST_APPEND(prefix, ra->pref64_prefixes, p); + ra->n_pref64_prefixes++; + + log_radv(ra, "Added PREF64 prefix %s", strna(addr_p)); + } + + if (ra->state == RADV_STATE_IDLE) + return 0; + + if (ra->ra_sent == 0) + return 0; + + /* If RAs have already been sent, send an RA immediately to announce the newly-added route prefix */ + r = radv_send(ra, NULL, ra->lifetime_usec); + if (r < 0) + log_radv_errno(ra, r, "Unable to send Router Advertisement for added PREF64 prefix %s, ignoring: %m", + strna(addr_p)); + else + log_radv(ra, "Sent Router Advertisement for added PREF64 prefix %s.", strna(addr_p)); + + return 0; +} + +int sd_radv_set_rdnss( + sd_radv *ra, + uint64_t lifetime_usec, + const struct in6_addr *dns, + size_t n_dns) { + + _cleanup_free_ struct sd_radv_opt_dns *opt_rdnss = NULL; + size_t len; + + assert_return(ra, -EINVAL); + assert_return(n_dns < 128, -EINVAL); + + if (lifetime_usec > RADV_RDNSS_MAX_LIFETIME_USEC) + return -EINVAL; + + if (!dns || n_dns == 0) { + ra->rdnss = mfree(ra->rdnss); + ra->n_rdnss = 0; + + return 0; + } + + len = sizeof(struct sd_radv_opt_dns) + sizeof(struct in6_addr) * n_dns; + + opt_rdnss = malloc0(len); + if (!opt_rdnss) + return -ENOMEM; + + opt_rdnss->type = RADV_OPT_RDNSS; + opt_rdnss->length = len / 8; + opt_rdnss->lifetime = usec_to_be32_sec(lifetime_usec); + + memcpy(opt_rdnss + 1, dns, n_dns * sizeof(struct in6_addr)); + + free_and_replace(ra->rdnss, opt_rdnss); + + ra->n_rdnss = n_dns; + + return 0; +} + +int sd_radv_set_dnssl( + sd_radv *ra, + uint64_t lifetime_usec, + char **search_list) { + + _cleanup_free_ struct sd_radv_opt_dns *opt_dnssl = NULL; + size_t len = 0; + uint8_t *p; + + assert_return(ra, -EINVAL); + + if (lifetime_usec > RADV_DNSSL_MAX_LIFETIME_USEC) + return -EINVAL; + + if (strv_isempty(search_list)) { + ra->dnssl = mfree(ra->dnssl); + return 0; + } + + STRV_FOREACH(s, search_list) + len += strlen(*s) + 2; + + len = (sizeof(struct sd_radv_opt_dns) + len + 7) & ~0x7; + + opt_dnssl = malloc0(len); + if (!opt_dnssl) + return -ENOMEM; + + opt_dnssl->type = RADV_OPT_DNSSL; + opt_dnssl->length = len / 8; + opt_dnssl->lifetime = usec_to_be32_sec(lifetime_usec); + + p = (uint8_t *)(opt_dnssl + 1); + len -= sizeof(struct sd_radv_opt_dns); + + STRV_FOREACH(s, search_list) { + int r; + + r = dns_name_to_wire_format(*s, p, len, false); + if (r < 0) + return r; + + if (len < (size_t)r) + return -ENOBUFS; + + p += r; + len -= r; + } + + free_and_replace(ra->dnssl, opt_dnssl); + + return 0; +} + +int sd_radv_prefix_new(sd_radv_prefix **ret) { + sd_radv_prefix *p; + + assert_return(ret, -EINVAL); + + p = new(sd_radv_prefix, 1); + if (!p) + return -ENOMEM; + + *p = (sd_radv_prefix) { + .n_ref = 1, + + .opt.type = ND_OPT_PREFIX_INFORMATION, + .opt.length = (sizeof(p->opt) - 1)/8 + 1, + .opt.prefixlen = 64, + + /* RFC 4861, Section 6.2.1 */ + .opt.flags = ND_OPT_PI_FLAG_ONLINK|ND_OPT_PI_FLAG_AUTO, + + .lifetime_valid_usec = RADV_DEFAULT_VALID_LIFETIME_USEC, + .lifetime_preferred_usec = RADV_DEFAULT_PREFERRED_LIFETIME_USEC, + .valid_until = USEC_INFINITY, + .preferred_until = USEC_INFINITY, + }; + + *ret = p; + return 0; +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_radv_prefix, sd_radv_prefix, mfree); + +int sd_radv_prefix_set_prefix( + sd_radv_prefix *p, + const struct in6_addr *in6_addr, + unsigned char prefixlen) { + + assert_return(p, -EINVAL); + assert_return(in6_addr, -EINVAL); + + if (prefixlen < 3 || prefixlen > 128) + return -EINVAL; + + if (prefixlen > 64) + /* unusual but allowed, log it */ + log_radv(NULL, "Unusual prefix length %d greater than 64", prefixlen); + + p->opt.in6_addr = *in6_addr; + p->opt.prefixlen = prefixlen; + + return 0; +} + +int sd_radv_prefix_get_prefix( + sd_radv_prefix *p, + struct in6_addr *ret_in6_addr, + unsigned char *ret_prefixlen) { + + assert_return(p, -EINVAL); + assert_return(ret_in6_addr, -EINVAL); + assert_return(ret_prefixlen, -EINVAL); + + *ret_in6_addr = p->opt.in6_addr; + *ret_prefixlen = p->opt.prefixlen; + + return 0; +} + +int sd_radv_prefix_set_onlink(sd_radv_prefix *p, int onlink) { + assert_return(p, -EINVAL); + + SET_FLAG(p->opt.flags, ND_OPT_PI_FLAG_ONLINK, onlink); + + return 0; +} + +int sd_radv_prefix_set_address_autoconfiguration(sd_radv_prefix *p, int address_autoconfiguration) { + assert_return(p, -EINVAL); + + SET_FLAG(p->opt.flags, ND_OPT_PI_FLAG_AUTO, address_autoconfiguration); + + return 0; +} + +int sd_radv_prefix_set_valid_lifetime(sd_radv_prefix *p, uint64_t lifetime_usec, uint64_t valid_until) { + assert_return(p, -EINVAL); + + p->lifetime_valid_usec = lifetime_usec; + p->valid_until = valid_until; + + return 0; +} + +int sd_radv_prefix_set_preferred_lifetime(sd_radv_prefix *p, uint64_t lifetime_usec, uint64_t valid_until) { + assert_return(p, -EINVAL); + + p->lifetime_preferred_usec = lifetime_usec; + p->preferred_until = valid_until; + + return 0; +} + +int sd_radv_route_prefix_new(sd_radv_route_prefix **ret) { + sd_radv_route_prefix *p; + + assert_return(ret, -EINVAL); + + p = new(sd_radv_route_prefix, 1); + if (!p) + return -ENOMEM; + + *p = (sd_radv_route_prefix) { + .n_ref = 1, + + .opt.type = RADV_OPT_ROUTE_INFORMATION, + .opt.length = DIV_ROUND_UP(sizeof(p->opt), 8), + .opt.prefixlen = 64, + + .lifetime_usec = RADV_DEFAULT_VALID_LIFETIME_USEC, + .valid_until = USEC_INFINITY, + }; + + *ret = p; + return 0; +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_radv_route_prefix, sd_radv_route_prefix, mfree); + +int sd_radv_route_prefix_set_prefix( + sd_radv_route_prefix *p, + const struct in6_addr *in6_addr, + unsigned char prefixlen) { + + assert_return(p, -EINVAL); + assert_return(in6_addr, -EINVAL); + + if (prefixlen > 128) + return -EINVAL; + + if (prefixlen > 64) + /* unusual but allowed, log it */ + log_radv(NULL, "Unusual prefix length %u greater than 64", prefixlen); + + p->opt.in6_addr = *in6_addr; + p->opt.prefixlen = prefixlen; + + return 0; +} + +int sd_radv_route_prefix_set_lifetime(sd_radv_route_prefix *p, uint64_t lifetime_usec, uint64_t valid_until) { + assert_return(p, -EINVAL); + + p->lifetime_usec = lifetime_usec; + p->valid_until = valid_until; + + return 0; +} + +int sd_radv_pref64_prefix_new(sd_radv_pref64_prefix **ret) { + sd_radv_pref64_prefix *p; + + assert_return(ret, -EINVAL); + + p = new(sd_radv_pref64_prefix, 1); + if (!p) + return -ENOMEM; + + *p = (sd_radv_pref64_prefix) { + .n_ref = 1, + + .opt.type = RADV_OPT_PREF64, + .opt.length = 2, + }; + + *ret = p; + return 0; +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_radv_pref64_prefix, sd_radv_pref64_prefix, mfree); + +int sd_radv_pref64_prefix_set_prefix( + sd_radv_pref64_prefix *p, + const struct in6_addr *prefix, + uint8_t prefixlen, + uint64_t lifetime_usec) { + + uint16_t pref64_lifetime; + uint8_t prefixlen_code; + int r; + + assert_return(p, -EINVAL); + assert_return(prefix, -EINVAL); + + r = pref64_prefix_length_to_plc(prefixlen, &prefixlen_code); + if (r < 0) + return log_radv_errno(NULL, r, + "Unsupported PREF64 prefix length %u. Valid lengths are 32, 40, 48, 56, 64 and 96", prefixlen); + + if (lifetime_usec > PREF64_MAX_LIFETIME_USEC) + return -EINVAL; + + /* RFC 8781 - 4.1 rounding up lifetime to multiply of 8 */ + pref64_lifetime = DIV_ROUND_UP(lifetime_usec, 8 * USEC_PER_SEC) << 3; + pref64_lifetime |= prefixlen_code; + + unaligned_write_be16(&p->opt.lifetime_and_plc, pref64_lifetime); + memcpy(&p->opt.prefix, prefix, sizeof(p->opt.prefix)); + + p->in6_addr = *prefix; + p->prefixlen = prefixlen; + + return 0; +} diff --git a/src/libsystemd-network/test-acd.c b/src/libsystemd-network/test-acd.c new file mode 100644 index 0000000..4b5ad70 --- /dev/null +++ b/src/libsystemd-network/test-acd.c @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <stdlib.h> +#include <unistd.h> + +#include <linux/veth.h> +#include <net/if.h> + +#include "sd-event.h" +#include "sd-ipv4acd.h" +#include "sd-netlink.h" + +#include "in-addr-util.h" +#include "tests.h" + +static void acd_handler(sd_ipv4acd *acd, int event, void *userdata) { + assert_se(acd); + + switch (event) { + case SD_IPV4ACD_EVENT_BIND: + log_info("bound"); + break; + case SD_IPV4ACD_EVENT_CONFLICT: + log_info("conflict"); + break; + case SD_IPV4ACD_EVENT_STOP: + log_error("the client was stopped"); + break; + default: + assert_not_reached(); + } +} + +static int client_run(int ifindex, const struct in_addr *pa, const struct ether_addr *ha, sd_event *e) { + sd_ipv4acd *acd; + + assert_se(sd_ipv4acd_new(&acd) >= 0); + assert_se(sd_ipv4acd_attach_event(acd, e, 0) >= 0); + + assert_se(sd_ipv4acd_set_ifindex(acd, ifindex) >= 0); + assert_se(sd_ipv4acd_set_mac(acd, ha) >= 0); + assert_se(sd_ipv4acd_set_address(acd, pa) >= 0); + assert_se(sd_ipv4acd_set_callback(acd, acd_handler, NULL) >= 0); + + log_info("starting IPv4ACD client"); + + assert_se(sd_ipv4acd_start(acd, true) >= 0); + + assert_se(sd_event_loop(e) >= 0); + + assert_se(!sd_ipv4acd_unref(acd)); + + return EXIT_SUCCESS; +} + +static int test_acd(const char *ifname, const char *address) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL, *reply = NULL; + union in_addr_union pa; + struct ether_addr ha; + int ifindex; + + assert_se(in_addr_from_string(AF_INET, address, &pa) >= 0); + + assert_se(sd_event_new(&e) >= 0); + + assert_se(sd_netlink_open(&rtnl) >= 0); + assert_se(sd_netlink_attach_event(rtnl, e, 0) >= 0); + + assert_se(sd_rtnl_message_new_link(rtnl, &m, RTM_GETLINK, 0) >= 0); + assert_se(sd_netlink_message_append_string(m, IFLA_IFNAME, ifname) >= 0); + assert_se(sd_netlink_call(rtnl, m, 0, &reply) >= 0); + + assert_se(sd_rtnl_message_link_get_ifindex(reply, &ifindex) >= 0); + assert_se(sd_netlink_message_read_ether_addr(reply, IFLA_ADDRESS, &ha) >= 0); + + client_run(ifindex, &pa.in, &ha, e); + + return EXIT_SUCCESS; +} + +int main(int argc, char *argv[]) { + test_setup_logging(LOG_DEBUG); + + if (argc == 3) + return test_acd(argv[1], argv[2]); + else { + log_error("This program takes two arguments.\n" + "\t %s <ifname> <IPv4 address>", program_invocation_short_name); + return EXIT_FAILURE; + } +} diff --git a/src/libsystemd-network/test-dhcp-client.c b/src/libsystemd-network/test-dhcp-client.c new file mode 100644 index 0000000..e3f148d --- /dev/null +++ b/src/libsystemd-network/test-dhcp-client.c @@ -0,0 +1,562 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <net/if.h> +#include <net/if_arp.h> +#include <stdio.h> +#include <sys/socket.h> +#include <unistd.h> +#if HAVE_VALGRIND_VALGRIND_H +# include <valgrind/valgrind.h> +#endif + +#include "sd-dhcp-client.h" +#include "sd-event.h" + +#include "alloc-util.h" +#include "dhcp-identifier.h" +#include "dhcp-network.h" +#include "dhcp-option.h" +#include "dhcp-packet.h" +#include "ether-addr-util.h" +#include "fd-util.h" +#include "random-util.h" +#include "tests.h" + +static struct hw_addr_data hw_addr = { + .length = ETH_ALEN, + .ether = {{ 'A', 'B', 'C', '1', '2', '3' }}, +}, bcast_addr = { + .length = ETH_ALEN, + .ether = {{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }}, +}; +typedef int (*test_callback_recv_t)(size_t size, DHCPMessage *dhcp); + +static bool verbose = true; +static int test_fd[2]; +static test_callback_recv_t callback_recv; +static be32_t xid; + +static void test_request_basic(sd_event *e) { + int r; + + sd_dhcp_client *client; + + if (verbose) + printf("* %s\n", __func__); + + /* Initialize client without Anonymize settings. */ + r = sd_dhcp_client_new(&client, false); + + assert_se(r >= 0); + assert_se(client); + + r = sd_dhcp_client_attach_event(client, e, 0); + assert_se(r >= 0); + + assert_se(sd_dhcp_client_set_request_option(NULL, 0) == -EINVAL); + assert_se(sd_dhcp_client_set_request_address(NULL, NULL) == -EINVAL); + assert_se(sd_dhcp_client_set_ifindex(NULL, 0) == -EINVAL); + + assert_se(sd_dhcp_client_set_ifindex(client, 15) == 0); + assert_se(sd_dhcp_client_set_ifindex(client, -42) == -EINVAL); + assert_se(sd_dhcp_client_set_ifindex(client, -1) == -EINVAL); + assert_se(sd_dhcp_client_set_ifindex(client, 0) == -EINVAL); + assert_se(sd_dhcp_client_set_ifindex(client, 1) == 0); + + assert_se(sd_dhcp_client_set_hostname(client, "host") == 1); + assert_se(sd_dhcp_client_set_hostname(client, "host.domain") == 1); + assert_se(sd_dhcp_client_set_hostname(client, NULL) == 1); + assert_se(sd_dhcp_client_set_hostname(client, "~host") == -EINVAL); + assert_se(sd_dhcp_client_set_hostname(client, "~host.domain") == -EINVAL); + + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_SUBNET_MASK) == 0); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_ROUTER) == 0); + /* This PRL option is not set when using Anonymize, but in this test + * Anonymize settings are not being used. */ + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_HOST_NAME) == 0); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_DOMAIN_NAME) == 0); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_DOMAIN_NAME_SERVER) == 0); + + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_PAD) == -EINVAL); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_END) == -EINVAL); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_MESSAGE_TYPE) == -EINVAL); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_OVERLOAD) == -EINVAL); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_PARAMETER_REQUEST_LIST) == -EINVAL); + + /* RFC7844: option 33 (SD_DHCP_OPTION_STATIC_ROUTE) is set in the + * default PRL when using Anonymize, so it is changed to other option + * that is not set by default, to check that it was set successfully. + * Options not set by default (using or not anonymize) are option 17 + * (SD_DHCP_OPTION_ROOT_PATH) and 42 (SD_DHCP_OPTION_NTP_SERVER) */ + assert_se(sd_dhcp_client_set_request_option(client, 17) == 1); + assert_se(sd_dhcp_client_set_request_option(client, 17) == 0); + assert_se(sd_dhcp_client_set_request_option(client, 42) == 1); + assert_se(sd_dhcp_client_set_request_option(client, 17) == 0); + + sd_dhcp_client_unref(client); +} + +static void test_request_anonymize(sd_event *e) { + int r; + + sd_dhcp_client *client; + + if (verbose) + printf("* %s\n", __func__); + + /* Initialize client with Anonymize settings. */ + r = sd_dhcp_client_new(&client, true); + + assert_se(r >= 0); + assert_se(client); + + r = sd_dhcp_client_attach_event(client, e, 0); + assert_se(r >= 0); + + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_NETBIOS_NAME_SERVER) == 0); + /* This PRL option is not set when using Anonymize */ + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_HOST_NAME) == 1); + assert_se(sd_dhcp_client_set_request_option(client, SD_DHCP_OPTION_PARAMETER_REQUEST_LIST) == -EINVAL); + + /* RFC7844: option 101 (SD_DHCP_OPTION_NEW_TZDB_TIMEZONE) is not set in the + * default PRL when using Anonymize, */ + assert_se(sd_dhcp_client_set_request_option(client, 101) == 1); + assert_se(sd_dhcp_client_set_request_option(client, 101) == 0); + + sd_dhcp_client_unref(client); +} + +static void test_checksum(void) { + uint8_t buf[20] = { + 0x45, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff + }; + + if (verbose) + printf("* %s\n", __func__); + + assert_se(dhcp_packet_checksum((uint8_t*)&buf, 20) == be16toh(0x78ae)); +} + +static void test_dhcp_identifier_set_iaid(void) { + uint32_t iaid_legacy; + be32_t iaid; + + assert_se(dhcp_identifier_set_iaid(NULL, &hw_addr, /* legacy = */ true, &iaid_legacy) >= 0); + assert_se(dhcp_identifier_set_iaid(NULL, &hw_addr, /* legacy = */ false, &iaid) >= 0); + + /* we expect, that the MAC address was hashed. The legacy value is in native + * endianness. */ + assert_se(iaid_legacy == 0x8dde4ba8u); + assert_se(iaid == htole32(0x8dde4ba8u)); +#if __BYTE_ORDER == __LITTLE_ENDIAN + assert_se(iaid == iaid_legacy); +#else + assert_se(iaid == bswap_32(iaid_legacy)); +#endif +} + +static int check_options(uint8_t code, uint8_t len, const void *option, void *userdata) { + switch (code) { + case SD_DHCP_OPTION_CLIENT_IDENTIFIER: + { + uint32_t iaid; + struct duid duid; + size_t duid_len; + + assert_se(dhcp_identifier_set_duid_en(&duid, &duid_len) >= 0); + assert_se(dhcp_identifier_set_iaid(NULL, &hw_addr, /* legacy = */ true, &iaid) >= 0); + + assert_se(len == sizeof(uint8_t) + sizeof(uint32_t) + duid_len); + assert_se(len == 19); + assert_se(((uint8_t*) option)[0] == 0xff); + + assert_se(memcmp((uint8_t*) option + 1, &iaid, sizeof(iaid)) == 0); + assert_se(memcmp((uint8_t*) option + 5, &duid, duid_len) == 0); + break; + } + + default: + break; + } + + return 0; +} + +int dhcp_network_send_raw_socket(int s, const union sockaddr_union *link, const void *packet, size_t len) { + size_t size; + _cleanup_free_ DHCPPacket *discover = NULL; + uint16_t ip_check, udp_check; + + assert_se(s >= 0); + assert_se(packet); + + size = sizeof(DHCPPacket); + assert_se(len > size); + + discover = memdup(packet, len); + + assert_se(discover->ip.ttl == IPDEFTTL); + assert_se(discover->ip.protocol == IPPROTO_UDP); + assert_se(discover->ip.saddr == INADDR_ANY); + assert_se(discover->ip.daddr == INADDR_BROADCAST); + assert_se(discover->udp.source == be16toh(DHCP_PORT_CLIENT)); + assert_se(discover->udp.dest == be16toh(DHCP_PORT_SERVER)); + + ip_check = discover->ip.check; + + discover->ip.ttl = 0; + discover->ip.check = discover->udp.len; + + udp_check = ~dhcp_packet_checksum((uint8_t*)&discover->ip.ttl, len - 8); + assert_se(udp_check == 0xffff); + + discover->ip.ttl = IPDEFTTL; + discover->ip.check = ip_check; + + ip_check = ~dhcp_packet_checksum((uint8_t*)&discover->ip, sizeof(discover->ip)); + assert_se(ip_check == 0xffff); + + assert_se(discover->dhcp.xid); + assert_se(memcmp(discover->dhcp.chaddr, hw_addr.bytes, hw_addr.length) == 0); + + size = len - sizeof(struct iphdr) - sizeof(struct udphdr); + + assert_se(callback_recv); + callback_recv(size, &discover->dhcp); + + return 575; +} + +int dhcp_network_bind_raw_socket( + int ifindex, + union sockaddr_union *link, + uint32_t id, + const struct hw_addr_data *_hw_addr, + const struct hw_addr_data *_bcast_addr, + uint16_t arp_type, + uint16_t port, + bool so_priority_set, + int so_priority) { + + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_fd) < 0) + return -errno; + + return test_fd[0]; +} + +int dhcp_network_bind_udp_socket(int ifindex, be32_t address, uint16_t port, int ip_service_type) { + int fd; + + fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + return fd; +} + +int dhcp_network_send_udp_socket(int s, be32_t address, uint16_t port, const void *packet, size_t len) { + return 0; +} + +static int test_discover_message_verify(size_t size, struct DHCPMessage *dhcp) { + int res; + + res = dhcp_option_parse(dhcp, size, check_options, NULL, NULL); + assert_se(res == DHCP_DISCOVER); + + if (verbose) + printf(" recv DHCP Discover 0x%08x\n", be32toh(dhcp->xid)); + + return 0; +} + +static void test_discover_message(sd_event *e) { + sd_dhcp_client *client; + int res, r; + + if (verbose) + printf("* %s\n", __func__); + + r = sd_dhcp_client_new(&client, false); + assert_se(r >= 0); + assert_se(client); + + r = sd_dhcp_client_attach_event(client, e, 0); + assert_se(r >= 0); + + assert_se(sd_dhcp_client_set_ifindex(client, 42) >= 0); + assert_se(sd_dhcp_client_set_mac(client, hw_addr.bytes, bcast_addr.bytes, hw_addr.length, ARPHRD_ETHER) >= 0); + + assert_se(sd_dhcp_client_set_request_option(client, 248) >= 0); + + callback_recv = test_discover_message_verify; + + res = sd_dhcp_client_start(client); + + assert_se(IN_SET(res, 0, -EINPROGRESS)); + + sd_event_run(e, UINT64_MAX); + + sd_dhcp_client_stop(client); + sd_dhcp_client_unref(client); + + test_fd[1] = safe_close(test_fd[1]); + + callback_recv = NULL; +} + +static uint8_t test_addr_acq_offer[] = { + 0x45, 0x10, 0x01, 0x48, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x11, 0xb3, 0x84, 0xc0, 0xa8, 0x02, 0x01, + 0xc0, 0xa8, 0x02, 0xbf, 0x00, 0x43, 0x00, 0x44, + 0x01, 0x34, 0x00, 0x00, 0x02, 0x01, 0x06, 0x00, + 0x6f, 0x95, 0x2f, 0x30, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x02, 0xbf, + 0xc0, 0xa8, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x63, 0x82, 0x53, 0x63, 0x35, 0x01, 0x02, 0x36, + 0x04, 0xc0, 0xa8, 0x02, 0x01, 0x33, 0x04, 0x00, + 0x00, 0x02, 0x58, 0x01, 0x04, 0xff, 0xff, 0xff, + 0x00, 0x2a, 0x04, 0xc0, 0xa8, 0x02, 0x01, 0x0f, + 0x09, 0x6c, 0x61, 0x62, 0x2e, 0x69, 0x6e, 0x74, + 0x72, 0x61, 0x03, 0x04, 0xc0, 0xa8, 0x02, 0x01, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static uint8_t test_addr_acq_ack[] = { + 0x45, 0x10, 0x01, 0x48, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x11, 0xb3, 0x84, 0xc0, 0xa8, 0x02, 0x01, + 0xc0, 0xa8, 0x02, 0xbf, 0x00, 0x43, 0x00, 0x44, + 0x01, 0x34, 0x00, 0x00, 0x02, 0x01, 0x06, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x02, 0xbf, + 0xc0, 0xa8, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x63, 0x82, 0x53, 0x63, 0x35, 0x01, 0x05, 0x36, + 0x04, 0xc0, 0xa8, 0x02, 0x01, 0x33, 0x04, 0x00, + 0x00, 0x02, 0x58, 0x01, 0x04, 0xff, 0xff, 0xff, + 0x00, 0x2a, 0x04, 0xc0, 0xa8, 0x02, 0x01, 0x0f, + 0x09, 0x6c, 0x61, 0x62, 0x2e, 0x69, 0x6e, 0x74, + 0x72, 0x61, 0x03, 0x04, 0xc0, 0xa8, 0x02, 0x01, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static int test_addr_acq_acquired(sd_dhcp_client *client, int event, + void *userdata) { + sd_event *e = userdata; + sd_dhcp_lease *lease; + struct in_addr addr; + const struct in_addr *addrs; + + assert_se(client); + assert_se(IN_SET(event, SD_DHCP_CLIENT_EVENT_IP_ACQUIRE, SD_DHCP_CLIENT_EVENT_SELECTING)); + + assert_se(sd_dhcp_client_get_lease(client, &lease) >= 0); + assert_se(lease); + + assert_se(sd_dhcp_lease_get_address(lease, &addr) >= 0); + assert_se(memcmp(&addr.s_addr, &test_addr_acq_ack[44], + sizeof(addr.s_addr)) == 0); + + assert_se(sd_dhcp_lease_get_netmask(lease, &addr) >= 0); + assert_se(memcmp(&addr.s_addr, &test_addr_acq_ack[285], + sizeof(addr.s_addr)) == 0); + + assert_se(sd_dhcp_lease_get_router(lease, &addrs) == 1); + assert_se(memcmp(&addrs[0].s_addr, &test_addr_acq_ack[308], + sizeof(addrs[0].s_addr)) == 0); + + if (verbose) + printf(" DHCP address acquired\n"); + + sd_event_exit(e, 0); + + return 0; +} + +static int test_addr_acq_recv_request(size_t size, DHCPMessage *request) { + uint16_t udp_check = 0; + uint8_t *msg_bytes = (uint8_t *)request; + int res; + + res = dhcp_option_parse(request, size, check_options, NULL, NULL); + assert_se(res == DHCP_REQUEST); + assert_se(xid == request->xid); + + assert_se(msg_bytes[size - 1] == SD_DHCP_OPTION_END); + + if (verbose) + printf(" recv DHCP Request 0x%08x\n", be32toh(xid)); + + memcpy(&test_addr_acq_ack[26], &udp_check, sizeof(udp_check)); + memcpy(&test_addr_acq_ack[32], &xid, sizeof(xid)); + memcpy(&test_addr_acq_ack[56], hw_addr.bytes, hw_addr.length); + + callback_recv = NULL; + + res = write(test_fd[1], test_addr_acq_ack, + sizeof(test_addr_acq_ack)); + assert_se(res == sizeof(test_addr_acq_ack)); + + if (verbose) + printf(" send DHCP Ack\n"); + + return 0; +}; + +static int test_addr_acq_recv_discover(size_t size, DHCPMessage *discover) { + uint16_t udp_check = 0; + uint8_t *msg_bytes = (uint8_t *)discover; + int res; + + res = dhcp_option_parse(discover, size, check_options, NULL, NULL); + assert_se(res == DHCP_DISCOVER); + + assert_se(msg_bytes[size - 1] == SD_DHCP_OPTION_END); + + xid = discover->xid; + + if (verbose) + printf(" recv DHCP Discover 0x%08x\n", be32toh(xid)); + + memcpy(&test_addr_acq_offer[26], &udp_check, sizeof(udp_check)); + memcpy(&test_addr_acq_offer[32], &xid, sizeof(xid)); + memcpy(&test_addr_acq_offer[56], hw_addr.bytes, hw_addr.length); + + callback_recv = test_addr_acq_recv_request; + + res = write(test_fd[1], test_addr_acq_offer, + sizeof(test_addr_acq_offer)); + assert_se(res == sizeof(test_addr_acq_offer)); + + if (verbose) + printf(" sent DHCP Offer\n"); + + return 0; +} + +static void test_addr_acq(sd_event *e) { + sd_dhcp_client *client; + int res, r; + + if (verbose) + printf("* %s\n", __func__); + + r = sd_dhcp_client_new(&client, false); + assert_se(r >= 0); + assert_se(client); + + r = sd_dhcp_client_attach_event(client, e, 0); + assert_se(r >= 0); + + assert_se(sd_dhcp_client_set_ifindex(client, 42) >= 0); + assert_se(sd_dhcp_client_set_mac(client, hw_addr.bytes, bcast_addr.bytes, hw_addr.length, ARPHRD_ETHER) >= 0); + + assert_se(sd_dhcp_client_set_callback(client, test_addr_acq_acquired, e) >= 0); + + callback_recv = test_addr_acq_recv_discover; + + assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, + 2 * USEC_PER_SEC, 0, + NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); + + res = sd_dhcp_client_start(client); + assert_se(IN_SET(res, 0, -EINPROGRESS)); + + assert_se(sd_event_loop(e) >= 0); + + assert_se(sd_dhcp_client_set_callback(client, NULL, NULL) >= 0); + assert_se(sd_dhcp_client_stop(client) >= 0); + sd_dhcp_client_unref(client); + + test_fd[1] = safe_close(test_fd[1]); + + callback_recv = NULL; + xid = 0; +} + +int main(int argc, char *argv[]) { + _cleanup_(sd_event_unrefp) sd_event *e; + + assert_se(setenv("SYSTEMD_NETWORK_TEST_MODE", "1", 1) >= 0); + + test_setup_logging(LOG_DEBUG); + + assert_se(sd_event_new(&e) >= 0); + + test_request_basic(e); + test_request_anonymize(e); + test_checksum(); + test_dhcp_identifier_set_iaid(); + + test_discover_message(e); + test_addr_acq(e); + +#if HAVE_VALGRIND_VALGRIND_H + /* Make sure the async_close thread has finished. + * valgrind would report some of the phread_* structures + * as not cleaned up properly. */ + if (RUNNING_ON_VALGRIND) + sleep(1); +#endif + + return 0; +} diff --git a/src/libsystemd-network/test-dhcp-option.c b/src/libsystemd-network/test-dhcp-option.c new file mode 100644 index 0000000..bcd46e4 --- /dev/null +++ b/src/libsystemd-network/test-dhcp-option.c @@ -0,0 +1,386 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <net/if_arp.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> + +#include "alloc-util.h" +#include "dhcp-option.h" +#include "dhcp-packet.h" +#include "ether-addr-util.h" +#include "macro.h" +#include "memory-util.h" +#include "tests.h" + +struct option_desc { + uint8_t sname[64]; + int snamelen; + uint8_t file[128]; + int filelen; + uint8_t options[128]; + int len; + bool success; + int filepos; + int snamepos; + int pos; +}; + +static bool verbose = false; + +static struct option_desc option_tests[] = { + { {}, 0, {}, 0, { 42, 5, 65, 66, 67, 68, 69 }, 7, false, }, + { {}, 0, {}, 0, { 42, 5, 65, 66, 67, 68, 69, 0, 0, + SD_DHCP_OPTION_MESSAGE_TYPE, 1, DHCP_ACK }, 12, true, }, + { {}, 0, {}, 0, { 8, 255, 70, 71, 72 }, 5, false, }, + { {}, 0, {}, 0, { 0x35, 0x01, 0x05, 0x36, 0x04, 0x01, 0x00, 0xa8, + 0xc0, 0x33, 0x04, 0x00, 0x01, 0x51, 0x80, 0x01, + 0x04, 0xff, 0xff, 0xff, 0x00, 0x03, 0x04, 0xc0, + 0xa8, 0x00, 0x01, 0x06, 0x04, 0xc0, 0xa8, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, + 40, true, }, + { {}, 0, {}, 0, { SD_DHCP_OPTION_MESSAGE_TYPE, 1, DHCP_OFFER, + 42, 3, 0, 0, 0 }, 8, true, }, + { {}, 0, {}, 0, { 42, 2, 1, 2, 44 }, 5, false, }, + + { {}, 0, + { 222, 3, 1, 2, 3, SD_DHCP_OPTION_MESSAGE_TYPE, 1, DHCP_NAK }, 8, + { SD_DHCP_OPTION_OVERLOAD, 1, DHCP_OVERLOAD_FILE }, 3, true, }, + + { { 1, 4, 1, 2, 3, 4, SD_DHCP_OPTION_MESSAGE_TYPE, 1, DHCP_ACK }, 9, + { 222, 3, 1, 2, 3 }, 5, + { SD_DHCP_OPTION_OVERLOAD, 1, + DHCP_OVERLOAD_FILE|DHCP_OVERLOAD_SNAME }, 3, true, }, +}; + +static const char *dhcp_type(int type) { + switch (type) { + case DHCP_DISCOVER: + return "DHCPDISCOVER"; + case DHCP_OFFER: + return "DHCPOFFER"; + case DHCP_REQUEST: + return "DHCPREQUEST"; + case DHCP_DECLINE: + return "DHCPDECLINE"; + case DHCP_ACK: + return "DHCPACK"; + case DHCP_NAK: + return "DHCPNAK"; + case DHCP_RELEASE: + return "DHCPRELEASE"; + default: + return "unknown"; + } +} + +static void test_invalid_buffer_length(void) { + DHCPMessage message; + + assert_se(dhcp_option_parse(&message, 0, NULL, NULL, NULL) == -EINVAL); + assert_se(dhcp_option_parse(&message, sizeof(DHCPMessage) - 1, NULL, NULL, NULL) == -EINVAL); +} + +static void test_message_init(void) { + _cleanup_free_ DHCPMessage *message = NULL; + size_t optlen = 4, optoffset; + size_t len = sizeof(DHCPMessage) + optlen; + uint8_t *magic; + + message = malloc0(len); + + assert_se(dhcp_message_init(message, BOOTREQUEST, 0x12345678, + DHCP_DISCOVER, ARPHRD_ETHER, ETH_ALEN, (uint8_t[16]){}, + optlen, &optoffset) >= 0); + + assert_se(message->xid == htobe32(0x12345678)); + assert_se(message->op == BOOTREQUEST); + + magic = (uint8_t*)&message->magic; + + assert_se(magic[0] == 99); + assert_se(magic[1] == 130); + assert_se(magic[2] == 83); + assert_se(magic[3] == 99); + + assert_se(dhcp_option_parse(message, len, NULL, NULL, NULL) >= 0); +} + +static DHCPMessage *create_message(uint8_t *options, uint16_t optlen, + uint8_t *file, uint8_t filelen, + uint8_t *sname, uint8_t snamelen) { + DHCPMessage *message; + size_t len = sizeof(DHCPMessage) + optlen; + + message = malloc0(len); + assert_se(message); + + memcpy_safe(&message->options, options, optlen); + memcpy_safe(&message->file, file, filelen); + memcpy_safe(&message->sname, sname, snamelen); + + return message; +} + +static void test_ignore_opts(uint8_t *descoption, int *descpos, int *desclen) { + assert_se(*descpos >= 0); + + while (*descpos < *desclen) { + switch (descoption[*descpos]) { + case SD_DHCP_OPTION_PAD: + *descpos += 1; + break; + + case SD_DHCP_OPTION_MESSAGE_TYPE: + case SD_DHCP_OPTION_OVERLOAD: + *descpos += 3; + break; + + default: + return; + } + } +} + +static int test_options_cb(uint8_t code, uint8_t len, const void *option, void *userdata) { + struct option_desc *desc = userdata; + uint8_t *descoption = NULL; + int *desclen = NULL, *descpos = NULL; + uint8_t optcode = 0; + uint8_t optlen = 0; + + assert_se((!desc && !code && !len) || desc); + + if (!desc) + return -EINVAL; + + assert_se(code != SD_DHCP_OPTION_PAD); + assert_se(code != SD_DHCP_OPTION_END); + assert_se(code != SD_DHCP_OPTION_MESSAGE_TYPE); + assert_se(code != SD_DHCP_OPTION_OVERLOAD); + + while (desc->pos >= 0 || desc->filepos >= 0 || desc->snamepos >= 0) { + + if (desc->pos >= 0) { + descoption = &desc->options[0]; + desclen = &desc->len; + descpos = &desc->pos; + } else if (desc->filepos >= 0) { + descoption = &desc->file[0]; + desclen = &desc->filelen; + descpos = &desc->filepos; + } else if (desc->snamepos >= 0) { + descoption = &desc->sname[0]; + desclen = &desc->snamelen; + descpos = &desc->snamepos; + } + + assert_se(descoption && desclen && descpos); + + if (*desclen) + test_ignore_opts(descoption, descpos, desclen); + + if (*descpos < *desclen) + break; + + if (*descpos == *desclen) + *descpos = -1; + } + + assert_se(descpos); + assert_se(*descpos != -1); + + optcode = descoption[*descpos]; + optlen = descoption[*descpos + 1]; + + if (verbose) + printf("DHCP code %2d(%2d) len %2d(%2d) ", code, optcode, + len, optlen); + + assert_se(code == optcode); + assert_se(len == optlen); + + for (unsigned i = 0; i < len; i++) { + if (verbose) + printf("0x%02x(0x%02x) ", + ((uint8_t*) option)[i], + descoption[*descpos + 2 + i]); + + assert_se(((uint8_t*) option)[i] == descoption[*descpos + 2 + i]); + } + + if (verbose) + printf("\n"); + + *descpos += optlen + 2; + + test_ignore_opts(descoption, descpos, desclen); + + if (desc->pos != -1 && desc->pos == desc->len) + desc->pos = -1; + + if (desc->filepos != -1 && desc->filepos == desc->filelen) + desc->filepos = -1; + + if (desc->snamepos != -1 && desc->snamepos == desc->snamelen) + desc->snamepos = -1; + + return 0; +} + +static void test_options(struct option_desc *desc) { + uint8_t *options = NULL; + uint8_t *file = NULL; + uint8_t *sname = NULL; + int optlen = 0; + int filelen = 0; + int snamelen = 0; + int buflen = 0; + _cleanup_free_ DHCPMessage *message = NULL; + int res; + + if (desc) { + file = &desc->file[0]; + filelen = desc->filelen; + if (!filelen) + desc->filepos = -1; + + sname = &desc->sname[0]; + snamelen = desc->snamelen; + if (!snamelen) + desc->snamepos = -1; + + options = &desc->options[0]; + optlen = desc->len; + desc->pos = 0; + } + message = create_message(options, optlen, file, filelen, + sname, snamelen); + + buflen = sizeof(DHCPMessage) + optlen; + + if (!desc) { + assert_se((res = dhcp_option_parse(message, buflen, test_options_cb, NULL, NULL)) == -ENOMSG); + } else if (desc->success) { + assert_se((res = dhcp_option_parse(message, buflen, test_options_cb, desc, NULL)) >= 0); + assert_se(desc->pos == -1 && desc->filepos == -1 && desc->snamepos == -1); + } else + assert_se((res = dhcp_option_parse(message, buflen, test_options_cb, desc, NULL)) < 0); + + if (verbose) + printf("DHCP type %s\n", dhcp_type(res)); +} + +static void test_option_removal(struct option_desc *desc) { + _cleanup_free_ DHCPMessage *message = create_message(&desc->options[0], desc->len, NULL, 0, NULL, 0); + + assert_se(dhcp_option_parse(message, sizeof(DHCPMessage) + desc->len, NULL, NULL, NULL) >= 0); + assert_se((desc->len = dhcp_option_remove_option(message->options, desc->len, SD_DHCP_OPTION_MESSAGE_TYPE)) >= 0); + assert_se(dhcp_option_parse(message, sizeof(DHCPMessage) + desc->len, NULL, NULL, NULL) < 0); +} + +static uint8_t the_options[64] = { + 'A', 'B', 'C', 'D', + 160, 2, 0x11, 0x12, + 0, + 31, 8, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0, + 55, 3, 0x51, 0x52, 0x53, + 17, 7, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 255 +}; + +static void test_option_set(void) { + _cleanup_free_ DHCPMessage *result = NULL; + size_t offset = 0, len, pos; + + result = malloc0(sizeof(DHCPMessage) + 11); + assert_se(result); + + result->options[0] = 'A'; + result->options[1] = 'B'; + result->options[2] = 'C'; + result->options[3] = 'D'; + + assert_se(dhcp_option_append(result, 0, &offset, 0, SD_DHCP_OPTION_PAD, + 0, NULL) == -ENOBUFS); + assert_se(offset == 0); + + offset = 4; + assert_se(dhcp_option_append(result, 5, &offset, 0, SD_DHCP_OPTION_PAD, + 0, NULL) == -ENOBUFS); + assert_se(offset == 4); + assert_se(dhcp_option_append(result, 6, &offset, 0, SD_DHCP_OPTION_PAD, + 0, NULL) >= 0); + assert_se(offset == 5); + + offset = pos = 4; + len = 11; + while (pos < len && the_options[pos] != SD_DHCP_OPTION_END) { + assert_se(dhcp_option_append(result, len, &offset, DHCP_OVERLOAD_SNAME, + the_options[pos], + the_options[pos + 1], + &the_options[pos + 2]) >= 0); + + if (the_options[pos] == SD_DHCP_OPTION_PAD) + pos++; + else + pos += 2 + the_options[pos + 1]; + + if (pos < len) + assert_se(offset == pos); + } + + for (unsigned i = 0; i < 9; i++) { + if (verbose) + printf("%2u: 0x%02x(0x%02x) (options)\n", i, result->options[i], + the_options[i]); + assert_se(result->options[i] == the_options[i]); + } + + if (verbose) + printf("%2d: 0x%02x(0x%02x) (options)\n", 9, result->options[9], + (unsigned) SD_DHCP_OPTION_END); + + assert_se(result->options[9] == SD_DHCP_OPTION_END); + + if (verbose) + printf("%2d: 0x%02x(0x%02x) (options)\n", 10, result->options[10], + (unsigned) SD_DHCP_OPTION_PAD); + + assert_se(result->options[10] == SD_DHCP_OPTION_PAD); + + for (unsigned i = 0; i < pos - 8; i++) { + if (verbose) + printf("%2u: 0x%02x(0x%02x) (sname)\n", i, result->sname[i], + the_options[i + 9]); + assert_se(result->sname[i] == the_options[i + 9]); + } + + if (verbose) + printf ("\n"); +} + +int main(int argc, char *argv[]) { + test_setup_logging(LOG_DEBUG); + + test_invalid_buffer_length(); + test_message_init(); + + test_options(NULL); + + for (unsigned i = 0; i < ELEMENTSOF(option_tests); i++) + test_options(&option_tests[i]); + + test_option_set(); + + for (unsigned i = 0; i < ELEMENTSOF(option_tests); i++) { + struct option_desc *desc = &option_tests[i]; + if (!desc->success || desc->snamelen > 0 || desc->filelen > 0) + continue; + test_option_removal(desc); + } + + return 0; +} diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c new file mode 100644 index 0000000..b2e6034 --- /dev/null +++ b/src/libsystemd-network/test-dhcp-server.c @@ -0,0 +1,330 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2013 Intel Corporation. All rights reserved. +***/ + +#include <errno.h> +#include <net/if_arp.h> + +#include "sd-dhcp-server.h" +#include "sd-event.h" + +#include "dhcp-server-internal.h" +#include "tests.h" + +static void test_pool(struct in_addr *address, unsigned size, int ret) { + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; + + assert_se(sd_dhcp_server_new(&server, 1) >= 0); + + assert_se(sd_dhcp_server_configure_pool(server, address, 8, 0, size) == ret); +} + +static int test_basic(bool bind_to_interface) { + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + struct in_addr address_lo = { + .s_addr = htobe32(INADDR_LOOPBACK), + }; + struct in_addr address_any = { + .s_addr = htobe32(INADDR_ANY), + }; + int r; + + log_debug("/* %s(bind_to_interface=%s) */", __func__, yes_no(bind_to_interface)); + + assert_se(sd_event_new(&event) >= 0); + + /* attach to loopback interface */ + assert_se(sd_dhcp_server_new(&server, 1) >= 0); + assert_se(server); + server->bind_to_interface = bind_to_interface; + + assert_se(sd_dhcp_server_attach_event(server, event, 0) >= 0); + assert_se(sd_dhcp_server_attach_event(server, event, 0) == -EBUSY); + assert_se(sd_dhcp_server_get_event(server) == event); + assert_se(sd_dhcp_server_detach_event(server) >= 0); + assert_se(!sd_dhcp_server_get_event(server)); + assert_se(sd_dhcp_server_attach_event(server, NULL, 0) >= 0); + assert_se(sd_dhcp_server_attach_event(server, NULL, 0) == -EBUSY); + + assert_se(sd_dhcp_server_ref(server) == server); + assert_se(!sd_dhcp_server_unref(server)); + + assert_se(sd_dhcp_server_start(server) == -EUNATCH); + + assert_se(sd_dhcp_server_configure_pool(server, &address_any, 28, 0, 0) == -EINVAL); + assert_se(sd_dhcp_server_configure_pool(server, &address_lo, 38, 0, 0) == -ERANGE); + assert_se(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0) >= 0); + assert_se(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0) >= 0); + + test_pool(&address_any, 1, -EINVAL); + test_pool(&address_lo, 1, 0); + + r = sd_dhcp_server_start(server); + if (r == -EPERM) + return r; + assert_se(r >= 0); + + assert_se(sd_dhcp_server_start(server) >= 0); + assert_se(sd_dhcp_server_stop(server) >= 0); + assert_se(sd_dhcp_server_stop(server) >= 0); + assert_se(sd_dhcp_server_start(server) >= 0); + + return 0; +} + +static void test_message_handler(void) { + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; + struct { + DHCPMessage message; + struct { + uint8_t code; + uint8_t length; + uint8_t type; + } _packed_ option_type; + struct { + uint8_t code; + uint8_t length; + be32_t address; + } _packed_ option_requested_ip; + struct { + uint8_t code; + uint8_t length; + be32_t address; + } _packed_ option_server_id; + struct { + uint8_t code; + uint8_t length; + uint8_t id[7]; + } _packed_ option_client_id; + struct { + uint8_t code; + uint8_t length; + uint8_t hostname[6]; + } _packed_ option_hostname; + uint8_t end; + } _packed_ test = { + .message.op = BOOTREQUEST, + .message.htype = ARPHRD_ETHER, + .message.hlen = ETHER_ADDR_LEN, + .message.xid = htobe32(0x12345678), + .message.chaddr = { 'A', 'B', 'C', 'D', 'E', 'F' }, + .option_type.code = SD_DHCP_OPTION_MESSAGE_TYPE, + .option_type.length = 1, + .option_type.type = DHCP_DISCOVER, + .option_hostname.code = SD_DHCP_OPTION_HOST_NAME, + .option_hostname.length = 6, + .option_hostname.hostname = { 'T', 'E', 'S', 'T', 'H', 'N' }, + .end = SD_DHCP_OPTION_END, + }; + struct in_addr address_lo = { + .s_addr = htobe32(INADDR_LOOPBACK), + }; + struct in_addr static_lease_address = { + .s_addr = htobe32(INADDR_LOOPBACK + 42), + }; + static uint8_t static_lease_client_id[7] = {0x01, 'A', 'B', 'C', 'D', 'E', 'G' }; + + log_debug("/* %s */", __func__); + + assert_se(sd_dhcp_server_new(&server, 1) >= 0); + assert_se(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0) >= 0); + assert_se(sd_dhcp_server_set_static_lease(server, &static_lease_address, static_lease_client_id, + ELEMENTSOF(static_lease_client_id)) >= 0); + assert_se(sd_dhcp_server_attach_event(server, NULL, 0) >= 0); + assert_se(sd_dhcp_server_start(server) >= 0); + + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + + test.end = 0; + /* TODO, shouldn't this fail? */ + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + test.end = SD_DHCP_OPTION_END; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + + test.option_type.code = 0; + test.option_type.length = 0; + test.option_type.type = 0; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == -ENOMSG); + test.option_type.code = SD_DHCP_OPTION_MESSAGE_TYPE; + test.option_type.length = 1; + test.option_type.type = DHCP_DISCOVER; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + + test.message.op = 0; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == 0); + test.message.op = BOOTREQUEST; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + + test.message.htype = 0; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + test.message.htype = ARPHRD_ETHER; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + + test.message.hlen = 0; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == -EBADMSG); + test.message.hlen = ETHER_ADDR_LEN; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_OFFER); + + test.option_type.type = DHCP_REQUEST; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == 0); + test.option_requested_ip.code = SD_DHCP_OPTION_REQUESTED_IP_ADDRESS; + test.option_requested_ip.length = 4; + test.option_requested_ip.address = htobe32(0x12345678); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_NAK); + test.option_server_id.code = SD_DHCP_OPTION_SERVER_IDENTIFIER; + test.option_server_id.length = 4; + test.option_server_id.address = htobe32(INADDR_LOOPBACK); + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 3); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_ACK); + + test.option_server_id.address = htobe32(0x12345678); + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 3); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == 0); + test.option_server_id.address = htobe32(INADDR_LOOPBACK); + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 4); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == 0); + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 3); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_ACK); + + test.option_client_id.code = SD_DHCP_OPTION_CLIENT_IDENTIFIER; + test.option_client_id.length = 7; + test.option_client_id.id[0] = 0x01; + test.option_client_id.id[1] = 'A'; + test.option_client_id.id[2] = 'B'; + test.option_client_id.id[3] = 'C'; + test.option_client_id.id[4] = 'D'; + test.option_client_id.id[5] = 'E'; + test.option_client_id.id[6] = 'F'; + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_ACK); + + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 30); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == 0); + + /* request address reserved for static lease (unmatching client ID) */ + test.option_client_id.id[6] = 'H'; + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 42); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == 0); + + /* request unmatching address */ + test.option_client_id.id[6] = 'G'; + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 41); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == 0); + + /* request matching address */ + test.option_client_id.id[6] = 'G'; + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 42); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_ACK); + + /* try again */ + test.option_client_id.id[6] = 'G'; + test.option_requested_ip.address = htobe32(INADDR_LOOPBACK + 42); + assert_se(dhcp_server_handle_message(server, (DHCPMessage*)&test, sizeof(test), NULL) == DHCP_ACK); +} + +static uint64_t client_id_hash_helper(DHCPClientId *id, uint8_t key[HASH_KEY_SIZE]) { + struct siphash state; + + siphash24_init(&state, key); + client_id_hash_func(id, &state); + + return htole64(siphash24_finalize(&state)); +} + +static void test_client_id_hash(void) { + DHCPClientId a = { + .length = 4, + }, b = { + .length = 4, + }; + uint8_t hash_key[HASH_KEY_SIZE] = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + }; + + log_debug("/* %s */", __func__); + + a.data = (uint8_t*)strdup("abcd"); + b.data = (uint8_t*)strdup("abcd"); + + assert_se(client_id_compare_func(&a, &b) == 0); + assert_se(client_id_hash_helper(&a, hash_key) == client_id_hash_helper(&b, hash_key)); + a.length = 3; + assert_se(client_id_compare_func(&a, &b) != 0); + a.length = 4; + assert_se(client_id_compare_func(&a, &b) == 0); + assert_se(client_id_hash_helper(&a, hash_key) == client_id_hash_helper(&b, hash_key)); + + b.length = 3; + assert_se(client_id_compare_func(&a, &b) != 0); + b.length = 4; + assert_se(client_id_compare_func(&a, &b) == 0); + assert_se(client_id_hash_helper(&a, hash_key) == client_id_hash_helper(&b, hash_key)); + + free(b.data); + b.data = (uint8_t*)strdup("abce"); + assert_se(client_id_compare_func(&a, &b) != 0); + + free(a.data); + free(b.data); +} + +static void test_static_lease(void) { + _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL; + + log_debug("/* %s */", __func__); + + assert_se(sd_dhcp_server_new(&server, 1) >= 0); + + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020304 }, + (uint8_t*) &(uint32_t) { 0x01020304 }, sizeof(uint32_t)) >= 0); + /* Duplicated entry. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020304 }, + (uint8_t*) &(uint32_t) { 0x01020304 }, sizeof(uint32_t)) == -EEXIST); + /* Address is conflicted. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020304 }, + (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t)) == -EEXIST); + /* Client ID is conflicted. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020305 }, + (uint8_t*) &(uint32_t) { 0x01020304 }, sizeof(uint32_t)) == -EEXIST); + + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020305 }, + (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t)) >= 0); + /* Remove the previous entry. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t)) >= 0); + /* Then, set a different address. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020306 }, + (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t)) >= 0); + /* Remove again. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t)) >= 0); + /* Try to remove non-existent entry. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t)) >= 0); + /* Try to remove non-existent entry. */ + assert_se(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t*) &(uint32_t) { 0x01020306 }, sizeof(uint32_t)) >= 0); +} + +int main(int argc, char *argv[]) { + int r; + + test_setup_logging(LOG_DEBUG); + + test_client_id_hash(); + test_static_lease(); + + r = test_basic(true); + if (r < 0) + return log_tests_skipped_errno(r, "cannot start dhcp server(bound to interface)"); + + r = test_basic(false); + if (r < 0) + return log_tests_skipped_errno(r, "cannot start dhcp server(non-bound to interface)"); + + test_message_handler(); + + return 0; +} diff --git a/src/libsystemd-network/test-dhcp6-client.c b/src/libsystemd-network/test-dhcp6-client.c new file mode 100644 index 0000000..ae3cdb8 --- /dev/null +++ b/src/libsystemd-network/test-dhcp6-client.c @@ -0,0 +1,1127 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <net/ethernet.h> +#include <net/if_arp.h> +#include <stdbool.h> +#include <stdio.h> +#include <sys/types.h> +#include <unistd.h> + +#include "sd-dhcp6-client.h" +#include "sd-event.h" + +#include "dhcp-identifier.h" +#include "dhcp6-internal.h" +#include "dhcp6-lease-internal.h" +#include "dhcp6-protocol.h" +#include "fd-util.h" +#include "macro.h" +#include "memory-util.h" +#include "socket-util.h" +#include "string-util.h" +#include "strv.h" +#include "tests.h" +#include "time-util.h" + +#define DHCP6_CLIENT_EVENT_TEST_ADVERTISED 77 +#define IA_ID_BYTES \ + 0x0e, 0xcf, 0xa3, 0x7d +#define IA_NA_ADDRESS1_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x78, 0xee, 0x1c, 0xf3, 0x09, 0x3c, 0x55, 0xad +#define IA_NA_ADDRESS2_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x78, 0xee, 0x1c, 0xf3, 0x09, 0x3c, 0x55, 0xae +#define IA_PD_PREFIX1_BYTES \ + 0x2a, 0x02, 0x81, 0x0d, 0x98, 0x80, 0x37, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +#define IA_PD_PREFIX2_BYTES \ + 0x2a, 0x02, 0x81, 0x0d, 0x98, 0x80, 0x37, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +#define DNS1_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 +#define DNS2_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 +#define SNTP1_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03 +#define SNTP2_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04 +#define NTP1_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05 +#define NTP2_BYTES \ + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06 +#define CLIENT_ID_BYTES \ + 0x00, 0x02, 0x00, 0x00, 0xab, 0x11, 0x61, 0x77, 0x40, 0xde, 0x13, 0x42, 0xc3, 0xa2 +#define SERVER_ID_BYTES \ + 0x00, 0x01, 0x00, 0x01, 0x19, 0x40, 0x5c, 0x53, 0x78, 0x2b, 0xcb, 0xb3, 0x6d, 0x53 +#define VENDOR_SUBOPTION_BYTES \ + 0x01 + +static const struct in6_addr local_address = + { { { 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, } } }; +static const struct in6_addr mcast_address = + IN6ADDR_ALL_DHCP6_RELAY_AGENTS_AND_SERVERS_INIT; +static const struct in6_addr ia_na_address1 = { { { IA_NA_ADDRESS1_BYTES } } }; +static const struct in6_addr ia_na_address2 = { { { IA_NA_ADDRESS2_BYTES } } }; +static const struct in6_addr ia_pd_prefix1 = { { { IA_PD_PREFIX1_BYTES } } }; +static const struct in6_addr ia_pd_prefix2 = { { { IA_PD_PREFIX2_BYTES } } }; +static const struct in6_addr dns1 = { { { DNS1_BYTES } } }; +static const struct in6_addr dns2 = { { { DNS2_BYTES } } }; +static const struct in6_addr sntp1 = { { { SNTP1_BYTES } } }; +static const struct in6_addr sntp2 = { { { SNTP2_BYTES } } }; +static const struct in6_addr ntp1 = { { { NTP1_BYTES } } }; +static const struct in6_addr ntp2 = { { { NTP2_BYTES } } }; +static const uint8_t client_id[] = { CLIENT_ID_BYTES }; +static const uint8_t server_id[] = { SERVER_ID_BYTES }; +static uint8_t vendor_suboption_data[] = { VENDOR_SUBOPTION_BYTES }; +static const struct ether_addr mac = { + .ether_addr_octet = { 'A', 'B', 'C', '1', '2', '3' }, +}; +static int test_fd[2] = EBADF_PAIR; +static sd_dhcp6_option vendor_suboption = { + .n_ref = 1, + .enterprise_identifier = 32, + .option = 247, + .data = vendor_suboption_data, + .length = 1, +}; +static int test_ifindex = 42; +static unsigned test_client_sent_message_count = 0; +static sd_dhcp6_client *client_ref = NULL; + +TEST(client_basic) { + _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL; + int v; + + assert_se(sd_dhcp6_client_new(&client) >= 0); + assert_se(client); + + assert_se(sd_dhcp6_client_set_ifindex(client, 15) == 0); + assert_se(sd_dhcp6_client_set_ifindex(client, 42) >= 0); + + assert_se(sd_dhcp6_client_set_mac(client, mac.ether_addr_octet, sizeof(mac), ARPHRD_ETHER) >= 0); + + assert_se(sd_dhcp6_client_set_fqdn(client, "host") == 1); + assert_se(sd_dhcp6_client_set_fqdn(client, "host.domain") == 1); + assert_se(sd_dhcp6_client_set_fqdn(client, NULL) == 1); + assert_se(sd_dhcp6_client_set_fqdn(client, "~host") == -EINVAL); + assert_se(sd_dhcp6_client_set_fqdn(client, "~host.domain") == -EINVAL); + + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_CLIENTID) == -EINVAL); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DNS_SERVER) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NTP_SERVER) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_SNTP_SERVER) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_VENDOR_OPTS) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DOMAIN) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, 10) == -EINVAL); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NIS_SERVER) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NISP_SERVER) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NIS_SERVER) == -EEXIST); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NISP_SERVER) == -EEXIST); + + assert_se(sd_dhcp6_client_set_information_request(client, 1) >= 0); + v = 0; + assert_se(sd_dhcp6_client_get_information_request(client, &v) >= 0); + assert_se(v); + assert_se(sd_dhcp6_client_set_information_request(client, 0) >= 0); + v = 42; + assert_se(sd_dhcp6_client_get_information_request(client, &v) >= 0); + assert_se(v == 0); + + v = 0; + assert_se(sd_dhcp6_client_get_address_request(client, &v) >= 0); + assert_se(v); + v = 0; + assert_se(sd_dhcp6_client_set_address_request(client, 1) >= 0); + assert_se(sd_dhcp6_client_get_address_request(client, &v) >= 0); + assert_se(v); + v = 42; + assert_se(sd_dhcp6_client_set_address_request(client, 1) >= 0); + assert_se(sd_dhcp6_client_get_address_request(client, &v) >= 0); + assert_se(v); + + assert_se(sd_dhcp6_client_set_address_request(client, 1) >= 0); + assert_se(sd_dhcp6_client_set_prefix_delegation(client, 1) >= 0); + v = 0; + assert_se(sd_dhcp6_client_get_address_request(client, &v) >= 0); + assert_se(v); + v = 0; + assert_se(sd_dhcp6_client_get_prefix_delegation(client, &v) >= 0); + assert_se(v); + + assert_se(sd_dhcp6_client_set_callback(client, NULL, NULL) >= 0); + + assert_se(sd_dhcp6_client_detach_event(client) >= 0); +} + +TEST(parse_domain) { + _cleanup_free_ char *domain = NULL; + _cleanup_strv_free_ char **list = NULL; + uint8_t *data; + + data = (uint8_t []) { 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0 }; + assert_se(dhcp6_option_parse_domainname(data, 13, &domain) >= 0); + assert_se(domain); + assert_se(streq(domain, "example.com")); + domain = mfree(domain); + + data = (uint8_t []) { 4, 't', 'e', 's', 't' }; + assert_se(dhcp6_option_parse_domainname(data, 5, &domain) >= 0); + assert_se(domain); + assert_se(streq(domain, "test")); + domain = mfree(domain); + + data = (uint8_t []) { 0 }; + assert_se(dhcp6_option_parse_domainname(data, 1, &domain) < 0); + + data = (uint8_t []) { 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, + 6, 'f', 'o', 'o', 'b', 'a', 'r', 0 }; + assert_se(dhcp6_option_parse_domainname_list(data, 21, &list) >= 0); + assert_se(list); + assert_se(streq(list[0], "example.com")); + assert_se(streq(list[1], "foobar")); + assert_se(!list[2]); + list = strv_free(list); + + data = (uint8_t []) { 1, 'a', 0, 20, 'b', 'c' }; + assert_se(dhcp6_option_parse_domainname_list(data, 6, &list) < 0); + + data = (uint8_t []) { 0 , 0 }; + assert_se(dhcp6_option_parse_domainname_list(data, 2, &list) < 0); +} + +TEST(option) { + static const uint8_t packet[] = { + 'F', 'O', 'O', 'H', 'O', 'G', 'E', + 0x00, SD_DHCP6_OPTION_ORO, 0x00, 0x07, + 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 0x00, SD_DHCP6_OPTION_VENDOR_CLASS, 0x00, 0x09, + '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'B', 'A', 'R', + }; + static const uint8_t result[] = { + 'F', 'O', 'O', 'H', 'O', 'G', 'E', + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 'B', 'A', 'R', + }; + _cleanup_free_ uint8_t *buf = NULL; + size_t offset, pos, optlen; + const uint8_t *optval; + uint16_t optcode; + + assert_se(sizeof(packet) == sizeof(result)); + + offset = 0; + assert_se(dhcp6_option_parse(packet, 0, &offset, &optcode, &optlen, &optval) == -EBADMSG); + + offset = 3; + assert_se(dhcp6_option_parse(packet, 0, &offset, &optcode, &optlen, &optval) == -EBADMSG); + + /* Tests for reading unaligned data. */ + assert_se(buf = new(uint8_t, sizeof(packet))); + for (size_t i = 0; i <= 7; i++) { + memcpy(buf, packet + i, sizeof(packet) - i); + offset = 7 - i; + assert_se(dhcp6_option_parse(buf, sizeof(packet), &offset, &optcode, &optlen, &optval) >= 0); + + assert_se(optcode == SD_DHCP6_OPTION_ORO); + assert_se(optlen == 7); + assert_se(optval == buf + 11 - i); + } + + offset = 7; + assert_se(dhcp6_option_parse(packet, sizeof(packet), &offset, &optcode, &optlen, &optval) >= 0); + + assert_se(optcode == SD_DHCP6_OPTION_ORO); + assert_se(optlen == 7); + assert_se(optval == packet + 11); + + free(buf); + assert_se(buf = memdup(result, sizeof(result))); + pos = 7; + assert_se(dhcp6_option_append(&buf, &pos, optcode, optlen, optval) >= 0); + + assert_se(dhcp6_option_parse(packet, sizeof(packet), &offset, &optcode, &optlen, &optval) >= 0); + + assert_se(optcode == SD_DHCP6_OPTION_VENDOR_CLASS); + assert_se(optlen == 9); + assert_se(optval == packet + 22); + + assert_se(dhcp6_option_append(&buf, &pos, optcode, optlen, optval) >= 0); + + assert_se(memcmp(packet, buf, sizeof(packet)) == 0); +} + +TEST(option_status) { + uint8_t option1[] = { + /* IA NA */ + 0x00, 0x03, 0x00, 0x12, 0x1a, 0x1d, 0x1a, 0x1d, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, + /* status option */ + 0x00, 0x0d, 0x00, 0x02, 0x00, 0x01, + }; + static const uint8_t option2[] = { + /* IA NA */ + 0x00, 0x03, 0x00, 0x2e, 0x1a, 0x1d, 0x1a, 0x1d, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, + /* IA Addr */ + 0x00, 0x05, 0x00, 0x1e, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x01, 0x02, 0x03, 0x04, 0x0a, 0x0b, 0x0c, 0x0d, + /* IA address status option */ + 0x00, 0x0d, 0x00, 0x02, 0x00, 0x01, + }; + static const uint8_t option3[] = { + /* IA NA */ + 0x00, 0x03, 0x00, 0x34, 0x1a, 0x1d, 0x1a, 0x1d, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, + /* IA Addr */ + 0x00, 0x05, 0x00, 0x24, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x01, 0x02, 0x03, 0x04, 0x0a, 0x0b, 0x0c, 0x0d, + /* IA address status option */ + 0x00, 0x0d, 0x00, 0x08, 0x00, 0x00, 'f', 'o', + 'o', 'b', 'a', 'r', + }; + static const uint8_t option4[] = { + /* IA PD */ + 0x00, 0x19, 0x00, 0x2f, 0x1a, 0x1d, 0x1a, 0x1d, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, + /* IA PD Prefix */ + 0x00, 0x1a, 0x00, 0x1f, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x80, 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, + 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + /* PD prefix status option */ + 0x00, 0x0d, 0x00, 0x02, 0x00, 0x00, + }; + static const uint8_t option5[] = { + /* IA PD */ + 0x00, 0x19, 0x00, 0x52, 0x1a, 0x1d, 0x1a, 0x1d, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, + /* IA PD Prefix #1 */ + 0x00, 0x1a, 0x00, 0x1f, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x80, 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, + 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + /* PD prefix status option */ + 0x00, 0x0d, 0x00, 0x02, 0x00, 0x00, + /* IA PD Prefix #2 */ + 0x00, 0x1a, 0x00, 0x1f, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x80, 0x20, 0x01, 0x0d, 0xb8, 0xc0, 0x0l, 0xd0, + 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + /* PD prefix status option */ + 0x00, 0x0d, 0x00, 0x02, 0x00, 0x00, + }; + _cleanup_(dhcp6_ia_freep) DHCP6IA *ia = NULL; + DHCP6Option *option; + be32_t iaid; + int r; + + memcpy(&iaid, option1 + 4, sizeof(iaid)); + + option = (DHCP6Option*) option1; + assert_se(sizeof(option1) == sizeof(DHCP6Option) + be16toh(option->len)); + + r = dhcp6_option_parse_ia(NULL, 0, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r == -ENOANO); + + r = dhcp6_option_parse_ia(NULL, iaid, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r == -EINVAL); + + option->len = htobe16(17); + r = dhcp6_option_parse_ia(NULL, iaid, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r == -EBADMSG); + + option->len = htobe16(sizeof(DHCP6Option)); + r = dhcp6_option_parse_ia(NULL, iaid, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r == -EBADMSG); + + option = (DHCP6Option*) option2; + assert_se(sizeof(option2) == sizeof(DHCP6Option) + be16toh(option->len)); + r = dhcp6_option_parse_ia(NULL, iaid, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r == -ENODATA); + + option = (DHCP6Option*) option3; + assert_se(sizeof(option3) == sizeof(DHCP6Option) + be16toh(option->len)); + r = dhcp6_option_parse_ia(NULL, iaid, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r >= 0); + assert_se(ia); + assert_se(ia->addresses); + ia = dhcp6_ia_free(ia); + + option = (DHCP6Option*) option4; + assert_se(sizeof(option4) == sizeof(DHCP6Option) + be16toh(option->len)); + r = dhcp6_option_parse_ia(NULL, iaid, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r >= 0); + assert_se(ia); + assert_se(ia->addresses); + assert_se(memcmp(&ia->header.id, &option4[4], 4) == 0); + assert_se(memcmp(&ia->header.lifetime_t1, &option4[8], 4) == 0); + assert_se(memcmp(&ia->header.lifetime_t2, &option4[12], 4) == 0); + ia = dhcp6_ia_free(ia); + + option = (DHCP6Option*) option5; + assert_se(sizeof(option5) == sizeof(DHCP6Option) + be16toh(option->len)); + r = dhcp6_option_parse_ia(NULL, iaid, be16toh(option->code), be16toh(option->len), option->data, &ia); + assert_se(r >= 0); + assert_se(ia); + assert_se(ia->addresses); + ia = dhcp6_ia_free(ia); +} + +TEST(client_parse_message_issue_22099) { + static const uint8_t msg[] = { + /* Message type */ + DHCP6_MESSAGE_REPLY, + /* Transaction ID */ + 0x7c, 0x4c, 0x16, + /* Rapid commit */ + 0x00, SD_DHCP6_OPTION_RAPID_COMMIT, 0x00, 0x00, + /* NTP servers */ + 0x00, SD_DHCP6_OPTION_NTP_SERVER, 0x00, 0x14, + /* NTP server (broken sub option and sub option length) */ + 0x01, 0x00, 0x10, 0x00, + 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0x15, 0xc8, 0xff, 0xfe, 0xef, 0x1e, 0x4e, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + 0x00, 0x02, /* DUID-EN */ + 0x00, 0x00, 0xab, 0x11, /* pen */ + 0x5c, 0x6b, 0x90, 0xec, 0xda, 0x95, 0x15, 0x45, /* id */ + /* Server ID */ + 0x00, SD_DHCP6_OPTION_SERVERID, 0x00, 0x0a, + 0x00, 0x03, /* DUID-LL */ + 0x00, 0x01, /* htype */ + 0xdc, 0x15, 0xc8, 0xef, 0x1e, 0x4e, /* haddr */ + /* preference */ + 0x00, SD_DHCP6_OPTION_PREFERENCE, 0x00, 0x01, + 0x00, + /* DNS servers */ + 0x00, SD_DHCP6_OPTION_DNS_SERVER, 0x00, 0x10, + 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0x15, 0xc8, 0xff, 0xfe, 0xef, 0x1e, 0x4e, + /* v6 pcp server */ + 0x00, SD_DHCP6_OPTION_V6_PCP_SERVER, 0x00, 0x10, + 0x2a, 0x02, 0x81, 0x0d, 0x98, 0x80, 0x37, 0x00, 0xde, 0x15, 0xc8, 0xff, 0xfe, 0xef, 0x1e, 0x4e, + /* IA_NA */ + 0x00, SD_DHCP6_OPTION_IA_NA, 0x00, 0x28, + 0xcc, 0x59, 0x11, 0x7b, /* iaid */ + 0x00, 0x00, 0x07, 0x08, /* lifetime T1 */ + 0x00, 0x00, 0x0b, 0x40, /* lifetime T2 */ + /* IA_NA (iaaddr suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + 0x2a, 0x02, 0x81, 0x0d, 0x98, 0x80, 0x37, 0x00, 0x6a, 0x05, 0xca, 0xff, 0xfe, 0xf1, 0x51, 0x53, /* address */ + 0x00, 0x00, 0x0e, 0x10, /* preferred lifetime */ + 0x00, 0x00, 0x1c, 0x20, /* valid lifetime */ + /* IA_PD */ + 0x00, SD_DHCP6_OPTION_IA_PD, 0x00, 0x29, + 0xcc, 0x59, 0x11, 0x7b, /* iaid */ + 0x00, 0x00, 0x07, 0x08, /* lifetime T1 */ + 0x00, 0x00, 0x0b, 0x40, /* lifetime T2 */ + /* IA_PD (iaprefix suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x0e, 0x10, /* preferred lifetime */ + 0x00, 0x00, 0x1c, 0x20, /* valid lifetime */ + 0x3a, /* prefixlen */ + 0x2a, 0x02, 0x81, 0x0d, 0x98, 0x80, 0x37, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* prefix */ + }; + static const uint8_t duid[] = { + 0x00, 0x00, 0xab, 0x11, 0x5c, 0x6b, 0x90, 0xec, 0xda, 0x95, 0x15, 0x45, + }; + _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL; + _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease = NULL; + + assert_se(sd_dhcp6_client_new(&client) >= 0); + assert_se(sd_dhcp6_client_set_iaid(client, 0xcc59117b) >= 0); + assert_se(sd_dhcp6_client_set_duid_raw(client, 2, duid, sizeof(duid)) >= 0); + + assert_se(dhcp6_lease_new_from_message(client, (const DHCP6Message*) msg, sizeof(msg), NULL, NULL, &lease) >= 0); +} + +TEST(client_parse_message_issue_24002) { + static const uint8_t msg[] = { + /* Message Type */ + 0x07, + /* Transaction ID */ + 0x0e, 0xa5, 0x7c, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + 0x00, 0x02, /* DUID-EN */ + 0x00, 0x00, 0xab, 0x11, /* pen */ + 0x5c, 0x6b, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, /* id */ + /* Server ID */ + 0x00, 0x02, 0x00, 0x1a, + 0x00, 0x02, 0x00, 0x00, 0x05, 0x83, 0x30, 0x63, 0x3a, 0x38, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + /* IA_PD */ + 0x00, 0x19, 0x00, 0x29, + 0xaa, 0xbb, 0xcc, 0xdd, /* iaid */ + 0x00, 0x00, 0x03, 0x84, /* lifetime (T1) */ + 0x00, 0x00, 0x05, 0xa0, /* lifetime (T2) */ + /* IA_PD (iaprefix suboption) */ + 0x00, 0x1a, 0x00, 0x19, + 0x00, 0x00, 0x07, 0x08, /* preferred lifetime */ + 0x00, 0x00, 0x38, 0x40, /* valid lifetime */ + 0x38, /* prefixlen */ + 0x20, 0x03, 0x00, 0xff, 0xaa, 0xbb, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* prefix */ + /* Rapid commit */ + 0x00, 0x0e, 0x00, 0x00, + /* Trailing invalid byte at the end. See issue #28183. */ + 00, + }; + static const uint8_t duid[] = { + 0x00, 0x00, 0xab, 0x11, 0x5c, 0x6b, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + }; + _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL; + _cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease = NULL; + + assert_se(sd_dhcp6_client_new(&client) >= 0); + assert_se(sd_dhcp6_client_set_iaid(client, 0xaabbccdd) >= 0); + assert_se(sd_dhcp6_client_set_duid_raw(client, 2, duid, sizeof(duid)) >= 0); + + assert_se(dhcp6_lease_new_from_message(client, (const DHCP6Message*) msg, sizeof(msg), NULL, NULL, &lease) >= 0); +} + +static const uint8_t msg_information_request[] = { + /* Message type */ + DHCP6_MESSAGE_INFORMATION_REQUEST, + /* Transaction ID */ + 0x0f, 0xb4, 0xe5, + /* MUD URL */ + /* ORO */ + 0x00, SD_DHCP6_OPTION_ORO, 0x00, 0x0c, + 0x00, SD_DHCP6_OPTION_DNS_SERVER, + 0x00, SD_DHCP6_OPTION_DOMAIN, + 0x00, SD_DHCP6_OPTION_SNTP_SERVER, + 0x00, SD_DHCP6_OPTION_INFORMATION_REFRESH_TIME, + 0x00, SD_DHCP6_OPTION_NTP_SERVER, + 0x00, SD_DHCP6_OPTION_INF_MAX_RT, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + CLIENT_ID_BYTES, + /* Extra options */ + /* Elapsed time */ + 0x00, SD_DHCP6_OPTION_ELAPSED_TIME, 0x00, 0x02, + 0x00, 0x00, +}; + +static const uint8_t msg_solicit[] = { + /* Message type */ + DHCP6_MESSAGE_SOLICIT, + /* Transaction ID */ + 0x0f, 0xb4, 0xe5, + /* Rapid commit */ + 0x00, SD_DHCP6_OPTION_RAPID_COMMIT, 0x00, 0x00, + /* IA_NA */ + 0x00, SD_DHCP6_OPTION_IA_NA, 0x00, 0x0c, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x00, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x00, /* lifetime T2 */ + /* IA_PD */ + 0x00, SD_DHCP6_OPTION_IA_PD, 0x00, 0x0c, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x00, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x00, /* lifetime T2 */ + /* Client FQDN */ + 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x11, + DHCP6_FQDN_FLAG_S, + 0x04, 'h', 'o', 's', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, + /* User Class */ + /* Vendor Class */ + /* Vendor Options */ + /* MUD URL */ + /* ORO */ + 0x00, SD_DHCP6_OPTION_ORO, 0x00, 0x0a, + 0x00, SD_DHCP6_OPTION_DNS_SERVER, + 0x00, SD_DHCP6_OPTION_DOMAIN, + 0x00, SD_DHCP6_OPTION_SNTP_SERVER, + 0x00, SD_DHCP6_OPTION_NTP_SERVER, + 0x00, SD_DHCP6_OPTION_SOL_MAX_RT, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + CLIENT_ID_BYTES, + /* Extra options */ + /* Elapsed time */ + 0x00, SD_DHCP6_OPTION_ELAPSED_TIME, 0x00, 0x02, + 0x00, 0x00, +}; + +static const uint8_t msg_request[] = { + /* Message type */ + DHCP6_MESSAGE_REQUEST, + /* Transaction ID */ + 0x00, 0x00, 0x00, + /* Server ID */ + 0x00, SD_DHCP6_OPTION_SERVERID, 0x00, 0x0e, + SERVER_ID_BYTES, + /* IA_NA */ + 0x00, SD_DHCP6_OPTION_IA_NA, 0x00, 0x44, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x00, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x00, /* lifetime T2 */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS1_BYTES, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS2_BYTES, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + /* IA_PD */ + 0x00, SD_DHCP6_OPTION_IA_PD, 0x00, 0x46, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x00, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x00, /* lifetime T2 */ + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX1_BYTES, + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX2_BYTES, + /* Client FQDN */ + 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x11, + DHCP6_FQDN_FLAG_S, + 0x04, 'h', 'o', 's', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, + /* User Class */ + /* Vendor Class */ + /* Vendor Options */ + /* MUD URL */ + /* ORO */ + 0x00, SD_DHCP6_OPTION_ORO, 0x00, 0x08, + 0x00, SD_DHCP6_OPTION_DNS_SERVER, + 0x00, SD_DHCP6_OPTION_DOMAIN, + 0x00, SD_DHCP6_OPTION_SNTP_SERVER, + 0x00, SD_DHCP6_OPTION_NTP_SERVER, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + CLIENT_ID_BYTES, + /* Extra options */ + /* Elapsed time */ + 0x00, SD_DHCP6_OPTION_ELAPSED_TIME, 0x00, 0x02, + 0x00, 0x00, +}; + +/* RFC 3315 section 18.1.6. The DHCP6 Release message must include: + - transaction id + - server identifier + - client identifier + - all released IA with addresses included + - elapsed time (required for all messages). + All other options aren't required. */ +static const uint8_t msg_release[] = { + /* Message type */ + DHCP6_MESSAGE_RELEASE, + /* Transaction ID */ + 0x00, 0x00, 0x00, + /* Server ID */ + 0x00, SD_DHCP6_OPTION_SERVERID, 0x00, 0x0e, + SERVER_ID_BYTES, + /* IA_NA */ + 0x00, SD_DHCP6_OPTION_IA_NA, 0x00, 0x44, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x00, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x00, /* lifetime T2 */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS1_BYTES, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS2_BYTES, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + /* IA_PD */ + 0x00, SD_DHCP6_OPTION_IA_PD, 0x00, 0x46, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x00, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x00, /* lifetime T2 */ + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX1_BYTES, + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x00, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0x00, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX2_BYTES, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + CLIENT_ID_BYTES, + /* Extra options */ + /* Elapsed time */ + 0x00, SD_DHCP6_OPTION_ELAPSED_TIME, 0x00, 0x02, + 0x00, 0x00, +}; + +static const uint8_t msg_reply[] = { + /* Message type */ + DHCP6_MESSAGE_REPLY, + /* Transaction ID */ + 0x0f, 0xb4, 0xe5, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + CLIENT_ID_BYTES, + /* Server ID */ + 0x00, SD_DHCP6_OPTION_SERVERID, 0x00, 0x0e, + SERVER_ID_BYTES, + /* Rapid commit */ + 0x00, SD_DHCP6_OPTION_RAPID_COMMIT, 0x00, 0x01, + 0x00, + /* IA_NA */ + 0x00, SD_DHCP6_OPTION_IA_NA, 0x00, 0x66, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x50, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x78, /* lifetime T2 */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS2_BYTES, + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS1_BYTES, + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + /* IA_NA (status code suboption) */ + 0x00, SD_DHCP6_OPTION_STATUS_CODE, 0x00, 0x1e, + 0x00, 0x00, /* status code */ + 0x41, 0x6c, 0x6c, 0x20, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x20, 0x77, 0x65, + 0x72, 0x65, 0x20, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x2e, /* status message */ + /* IA_PD */ + 0x00, SD_DHCP6_OPTION_IA_PD, 0x00, 0x46, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x50, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x78, /* lifetime T2 */ + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX2_BYTES, + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX1_BYTES, + /* DNS servers */ + 0x00, SD_DHCP6_OPTION_DNS_SERVER, 0x00, 0x20, + DNS1_BYTES, + DNS2_BYTES, + /* SNTP servers */ + 0x00, SD_DHCP6_OPTION_SNTP_SERVER, 0x00, 0x20, + SNTP1_BYTES, + SNTP2_BYTES, + /* NTP servers */ + 0x00, SD_DHCP6_OPTION_NTP_SERVER, 0x00, 0x37, + /* NTP server (address suboption) */ + 0x00, DHCP6_NTP_SUBOPTION_SRV_ADDR, 0x00, 0x10, + NTP1_BYTES, + /* NTP server (address suboption) */ + 0x00, DHCP6_NTP_SUBOPTION_SRV_ADDR, 0x00, 0x10, + NTP2_BYTES, + /* NTP server (fqdn suboption) */ + 0x00, DHCP6_NTP_SUBOPTION_SRV_FQDN, 0x00, 0x0b, + 0x03, 'n', 't', 'p', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, + /* Domain list */ + 0x00, SD_DHCP6_OPTION_DOMAIN, 0x00, 0x0b, + 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, + /* Client FQDN */ + 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x12, + 0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', + /* Vendor specific options */ + 0x00, SD_DHCP6_OPTION_VENDOR_OPTS, 0x00, 0x09, + 0x00, 0x00, 0x00, 0x20, 0x00, 0xf7, 0x00, 0x01, VENDOR_SUBOPTION_BYTES, +}; + +static const uint8_t msg_advertise[] = { + /* Message type */ + DHCP6_MESSAGE_ADVERTISE, + /* Transaction ID */ + 0x0f, 0xb4, 0xe5, + /* Client ID */ + 0x00, SD_DHCP6_OPTION_CLIENTID, 0x00, 0x0e, + CLIENT_ID_BYTES, + /* Server ID */ + 0x00, SD_DHCP6_OPTION_SERVERID, 0x00, 0x0e, + SERVER_ID_BYTES, + /* Preference */ + 0x00, SD_DHCP6_OPTION_PREFERENCE, 0x00, 0x01, + 0xff, + /* IA_NA */ + 0x00, SD_DHCP6_OPTION_IA_NA, 0x00, 0x7a, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x50, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x78, /* lifetime T2 */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS2_BYTES, /* address */ + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + /* IA_NA (IAADDR suboption) */ + 0x00, SD_DHCP6_OPTION_IAADDR, 0x00, 0x18, + IA_NA_ADDRESS1_BYTES, /* address */ + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + /* IA_NA (status code suboption) */ + 0x00, SD_DHCP6_OPTION_STATUS_CODE, 0x00, 0x32, + 0x00, 0x00, /* status code */ + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x28, 0x65, 0x73, 0x29, 0x20, 0x72, 0x65, 0x6e, 0x65, + 0x77, 0x65, 0x64, 0x2e, 0x20, 0x47, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x20, 0x66, + 0x72, 0x6f, 0x6d, 0x20, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x74, 0x20, 0x45, 0x61, 0x72, 0x74, 0x68, /* status message */ + /* IA_PD */ + 0x00, SD_DHCP6_OPTION_IA_PD, 0x00, 0x46, + IA_ID_BYTES, + 0x00, 0x00, 0x00, 0x50, /* lifetime T1 */ + 0x00, 0x00, 0x00, 0x78, /* lifetime T2 */ + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX2_BYTES, + /* IA_PD (IA_PD_PREFIX suboption) */ + 0x00, SD_DHCP6_OPTION_IA_PD_PREFIX, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x96, /* preferred lifetime */ + 0x00, 0x00, 0x00, 0xb4, /* valid lifetime */ + 0x40, /* prefixlen */ + IA_PD_PREFIX1_BYTES, + /* DNS servers */ + 0x00, SD_DHCP6_OPTION_DNS_SERVER, 0x00, 0x20, + DNS1_BYTES, + DNS2_BYTES, + /* SNTP servers */ + 0x00, SD_DHCP6_OPTION_SNTP_SERVER, 0x00, 0x20, + SNTP1_BYTES, + SNTP2_BYTES, + /* NTP servers */ + 0x00, SD_DHCP6_OPTION_NTP_SERVER, 0x00, 0x37, + /* NTP server (address suboption) */ + 0x00, DHCP6_NTP_SUBOPTION_SRV_ADDR, 0x00, 0x10, + NTP1_BYTES, + /* NTP server (address suboption) */ + 0x00, DHCP6_NTP_SUBOPTION_SRV_ADDR, 0x00, 0x10, + NTP2_BYTES, + /* NTP server (fqdn suboption) */ + 0x00, DHCP6_NTP_SUBOPTION_SRV_FQDN, 0x00, 0x0b, + 0x03, 'n', 't', 'p', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, + /* Domain list */ + 0x00, SD_DHCP6_OPTION_DOMAIN, 0x00, 0x0b, + 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, + /* Client FQDN */ + 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x12, + 0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', + /* Vendor specific options */ + 0x00, SD_DHCP6_OPTION_VENDOR_OPTS, 0x00, 0x09, + 0x00, 0x00, 0x00, 0x20, 0x00, 0xf7, 0x00, 0x01, VENDOR_SUBOPTION_BYTES, +}; + +static void test_client_verify_information_request(const DHCP6Message *msg, size_t len) { + log_debug("/* %s */", __func__); + + assert_se(len == sizeof(msg_information_request)); + /* The elapsed time value is not deterministic. Skip it. */ + assert_se(memcmp(msg, msg_information_request, len - sizeof(be16_t)) == 0); +} + +static void test_client_verify_solicit(const DHCP6Message *msg, size_t len) { + log_debug("/* %s */", __func__); + + assert_se(len == sizeof(msg_solicit)); + /* The elapsed time value is not deterministic. Skip it. */ + assert_se(memcmp(msg, msg_solicit, len - sizeof(be16_t)) == 0); +} + +static void test_client_verify_release(const DHCP6Message *msg, size_t len) { + log_debug("/* %s */", __func__); + + assert_se(len == sizeof(msg_release)); + assert_se(msg->type == DHCP6_MESSAGE_RELEASE); + /* The transaction ID and elapsed time value are not deterministic. Skip them. */ + assert_se(memcmp(msg->options, msg_release + offsetof(DHCP6Message, options), + len - offsetof(DHCP6Message, options) - sizeof(be16_t)) == 0); +} + +static void test_client_verify_request(const DHCP6Message *msg, size_t len) { + log_debug("/* %s */", __func__); + + assert_se(len == sizeof(msg_request)); + assert_se(msg->type == DHCP6_MESSAGE_REQUEST); + /* The transaction ID and elapsed time value are not deterministic. Skip them. */ + assert_se(memcmp(msg->options, msg_request + offsetof(DHCP6Message, options), + len - offsetof(DHCP6Message, options) - sizeof(be16_t)) == 0); +} + +static void test_lease_common(sd_dhcp6_client *client) { + sd_dhcp6_lease *lease; + sd_dhcp6_option **suboption; + const struct in6_addr *addrs; + const char *str; + char **strv; + uint8_t *id; + size_t len; + + assert_se(sd_dhcp6_client_get_lease(client, &lease) >= 0); + + assert_se(dhcp6_lease_get_clientid(lease, &id, &len) >= 0); + assert_se(memcmp_nn(id, len, client_id, sizeof(client_id)) == 0); + + assert_se(sd_dhcp6_lease_get_domains(lease, &strv) == 1); + assert_se(streq(strv[0], "lab.intra")); + assert_se(!strv[1]); + + assert_se(sd_dhcp6_lease_get_fqdn(lease, &str) >= 0); + assert_se(streq(str, "client.lab.intra")); + + assert_se(sd_dhcp6_lease_get_dns(lease, &addrs) == 2); + assert_se(in6_addr_equal(&addrs[0], &dns1)); + assert_se(in6_addr_equal(&addrs[1], &dns2)); + + assert_se(sd_dhcp6_lease_get_ntp_addrs(lease, &addrs) == 2); + assert_se(in6_addr_equal(&addrs[0], &ntp1)); + assert_se(in6_addr_equal(&addrs[1], &ntp2)); + + assert_se(sd_dhcp6_lease_get_ntp_fqdn(lease, &strv) == 1); + assert_se(streq(strv[0], "ntp.intra")); + assert_se(!strv[1]); + + assert_se(lease->sntp_count == 2); + assert_se(in6_addr_equal(&lease->sntp[0], &sntp1)); + assert_se(in6_addr_equal(&lease->sntp[1], &sntp2)); + + assert_se(sd_dhcp6_lease_get_vendor_options(lease, &suboption) > 0); + assert_se((*suboption)->enterprise_identifier == vendor_suboption.enterprise_identifier); + assert_se((*suboption)->option == vendor_suboption.option); + assert_se(*(uint8_t*)(*suboption)->data == *(uint8_t*)vendor_suboption.data); +} + +static void test_lease_managed(sd_dhcp6_client *client) { + sd_dhcp6_lease *lease; + struct in6_addr addr; + usec_t lt_pref, lt_valid; + uint8_t *id, prefixlen; + size_t len; + + assert_se(sd_dhcp6_client_get_lease(client, &lease) >= 0); + + assert_se(dhcp6_lease_get_serverid(lease, &id, &len) >= 0); + assert_se(memcmp_nn(id, len, server_id, sizeof(server_id)) == 0); + + assert_se(sd_dhcp6_lease_has_address(lease)); + assert_se(sd_dhcp6_lease_has_pd_prefix(lease)); + + for (unsigned i = 0; i < 2; i++) { + assert_se(sd_dhcp6_lease_address_iterator_reset(lease)); + assert_se(sd_dhcp6_lease_get_address(lease, &addr) >= 0); + assert_se(sd_dhcp6_lease_get_address_lifetime(lease, <_pref, <_valid) >= 0); + assert_se(in6_addr_equal(&addr, &ia_na_address1)); + assert_se(lt_pref == 150 * USEC_PER_SEC); + assert_se(lt_valid == 180 * USEC_PER_SEC); + assert_se(sd_dhcp6_lease_address_iterator_next(lease)); + assert_se(sd_dhcp6_lease_get_address(lease, &addr) >= 0); + assert_se(sd_dhcp6_lease_get_address_lifetime(lease, <_pref, <_valid) >= 0); + assert_se(in6_addr_equal(&addr, &ia_na_address2)); + assert_se(lt_pref == 150 * USEC_PER_SEC); + assert_se(lt_valid == 180 * USEC_PER_SEC); + assert_se(!sd_dhcp6_lease_address_iterator_next(lease)); + + assert_se(sd_dhcp6_lease_pd_iterator_reset(lease)); + assert_se(sd_dhcp6_lease_get_pd_prefix(lease, &addr, &prefixlen) >= 0); + assert_se(sd_dhcp6_lease_get_pd_lifetime(lease, <_pref, <_valid) >= 0); + assert_se(in6_addr_equal(&addr, &ia_pd_prefix1)); + assert_se(prefixlen == 64); + assert_se(lt_pref == 150 * USEC_PER_SEC); + assert_se(lt_valid == 180 * USEC_PER_SEC); + assert_se(sd_dhcp6_lease_pd_iterator_next(lease)); + assert_se(sd_dhcp6_lease_get_pd_prefix(lease, &addr, &prefixlen) >= 0); + assert_se(sd_dhcp6_lease_get_pd_lifetime(lease, <_pref, <_valid) >= 0); + assert_se(in6_addr_equal(&addr, &ia_pd_prefix2)); + assert_se(prefixlen == 64); + assert_se(lt_pref == 150 * USEC_PER_SEC); + assert_se(lt_valid == 180 * USEC_PER_SEC); + assert_se(!sd_dhcp6_lease_pd_iterator_next(lease)); + } + + test_lease_common(client); +} + +static void test_client_callback(sd_dhcp6_client *client, int event, void *userdata) { + switch (event) { + case SD_DHCP6_CLIENT_EVENT_STOP: + log_debug("/* %s (event=stop) */", __func__); + return; + + case SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST: + log_debug("/* %s (event=information-request) */", __func__); + + assert_se(test_client_sent_message_count == 1); + + test_lease_common(client); + + assert_se(sd_dhcp6_client_set_information_request(client, false) >= 0); + assert_se(sd_dhcp6_client_start(client) >= 0); + assert_se(dhcp6_client_set_transaction_id(client, ((const DHCP6Message*) msg_advertise)->transaction_id) >= 0); + break; + + case SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE: + log_debug("/* %s (event=ip-acquire) */", __func__); + + assert_se(IN_SET(test_client_sent_message_count, 3, 5)); + + test_lease_managed(client); + + switch (test_client_sent_message_count) { + case 3: + assert_se(sd_dhcp6_client_stop(client) >= 0); + assert_se(sd_dhcp6_client_start(client) >= 0); + assert_se(dhcp6_client_set_transaction_id(client, ((const DHCP6Message*) msg_reply)->transaction_id) >= 0); + break; + + case 5: + assert_se(sd_event_exit(sd_dhcp6_client_get_event(client), 0) >= 0); + break; + + default: + assert_not_reached(); + } + + break; + + case DHCP6_CLIENT_EVENT_TEST_ADVERTISED: { + sd_dhcp6_lease *lease; + uint8_t preference; + + log_debug("/* %s (event=test-advertised) */", __func__); + + assert_se(test_client_sent_message_count == 2); + + test_lease_managed(client); + + assert_se(sd_dhcp6_client_get_lease(client, &lease) >= 0); + assert_se(dhcp6_lease_get_preference(lease, &preference) >= 0); + assert_se(preference == 0xff); + + assert_se(dhcp6_client_set_transaction_id(client, ((const DHCP6Message*) msg_reply)->transaction_id) >= 0); + break; + } + default: + assert_not_reached(); + } +} + +int dhcp6_network_send_udp_socket(int s, struct in6_addr *a, const void *packet, size_t len) { + log_debug("/* %s(count=%u) */", __func__, test_client_sent_message_count); + + assert_se(a); + assert_se(in6_addr_equal(a, &mcast_address)); + assert_se(packet); + assert_se(len >= sizeof(DHCP6Message)); + + switch (test_client_sent_message_count) { + case 0: + test_client_verify_information_request(packet, len); + assert_se(write(test_fd[1], msg_reply, sizeof(msg_reply)) == sizeof(msg_reply)); + break; + + case 1: + test_client_verify_solicit(packet, len); + assert_se(write(test_fd[1], msg_advertise, sizeof(msg_advertise)) == sizeof(msg_advertise)); + break; + + case 2: + test_client_callback(client_ref, DHCP6_CLIENT_EVENT_TEST_ADVERTISED, NULL); + test_client_verify_request(packet, len); + assert_se(write(test_fd[1], msg_reply, sizeof(msg_reply)) == sizeof(msg_reply)); + break; + + case 3: + test_client_verify_release(packet, len); + /* when stopping, dhcp6 client doesn't wait for release server reply */ + assert_se(write(test_fd[1], msg_reply, sizeof(msg_reply)) == sizeof(msg_reply)); + break; + + case 4: + test_client_verify_solicit(packet, len); + assert_se(write(test_fd[1], msg_reply, sizeof(msg_reply)) == sizeof(msg_reply)); + break; + + default: + assert_not_reached(); + } + + test_client_sent_message_count++; + return len; +} + +int dhcp6_network_bind_udp_socket(int ifindex, struct in6_addr *a) { + assert_se(ifindex == test_ifindex); + assert_se(a); + assert_se(in6_addr_equal(a, &local_address)); + + assert_se(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_fd) >= 0); + return TAKE_FD(test_fd[0]); +} + +TEST(dhcp6_client) { + _cleanup_(sd_dhcp6_client_unrefp) sd_dhcp6_client *client = NULL; + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + + assert_se(sd_event_new(&e) >= 0); + assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, + 2 * USEC_PER_SEC, 0, + NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); + + assert_se(sd_dhcp6_client_new(&client) >= 0); + assert_se(sd_dhcp6_client_attach_event(client, e, 0) >= 0); + assert_se(sd_dhcp6_client_set_ifindex(client, test_ifindex) == 0); + assert_se(sd_dhcp6_client_set_local_address(client, &local_address) >= 0); + assert_se(sd_dhcp6_client_set_fqdn(client, "host.lab.intra") >= 0); + assert_se(sd_dhcp6_client_set_iaid(client, unaligned_read_be32((uint8_t[]) { IA_ID_BYTES })) >= 0); + assert_se(sd_dhcp6_client_set_send_release(client, true) >= 0); + + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DNS_SERVER) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_DOMAIN) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NTP_SERVER) >= 0); + assert_se(sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_SNTP_SERVER) >= 0); + + assert_se(sd_dhcp6_client_set_information_request(client, true) >= 0); + assert_se(sd_dhcp6_client_set_callback(client, test_client_callback, NULL) >= 0); + + assert_se(sd_dhcp6_client_start(client) >= 0); + + assert_se(dhcp6_client_set_transaction_id(client, ((const DHCP6Message*) msg_reply)->transaction_id) >= 0); + + assert_se(client_ref = sd_dhcp6_client_ref(client)); + + assert_se(sd_event_loop(e) >= 0); + + assert_se(test_client_sent_message_count == 5); + + assert_se(!sd_dhcp6_client_unref(client_ref)); + test_fd[1] = safe_close(test_fd[1]); +} + +static int intro(void) { + assert_se(setenv("SYSTEMD_NETWORK_TEST_MODE", "1", 1) >= 0); + return 0; +} + +DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro); diff --git a/src/libsystemd-network/test-ipv4ll-manual.c b/src/libsystemd-network/test-ipv4ll-manual.c new file mode 100644 index 0000000..5dc6b10 --- /dev/null +++ b/src/libsystemd-network/test-ipv4ll-manual.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <net/if.h> +#include <stdlib.h> +#include <unistd.h> +#include <linux/veth.h> + +#include "sd-event.h" +#include "sd-ipv4ll.h" +#include "sd-netlink.h" + +#include "alloc-util.h" +#include "in-addr-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "tests.h" + +static void ll_handler(sd_ipv4ll *ll, int event, void *userdata) { + assert_se(ll); + + struct in_addr addr; + const char *pretty = sd_ipv4ll_get_address(ll, &addr) >= 0 ? IN4_ADDR_TO_STRING(&addr) : NULL; + + switch (event) { + case SD_IPV4LL_EVENT_BIND: + log_info("bound %s", strna(pretty)); + break; + case SD_IPV4LL_EVENT_CONFLICT: + log_info("conflict on %s", strna(pretty)); + break; + case SD_IPV4LL_EVENT_STOP: + log_error("the client was stopped with address %s", strna(pretty)); + break; + default: + assert_not_reached(); + } +} + +static int client_run(int ifindex, const char *seed_str, const struct in_addr *start_address, const struct ether_addr *ha, sd_event *e) { + sd_ipv4ll *ll; + + assert_se(sd_ipv4ll_new(&ll) >= 0); + assert_se(sd_ipv4ll_attach_event(ll, e, 0) >= 0); + + assert_se(sd_ipv4ll_set_ifindex(ll, ifindex) >= 0); + assert_se(sd_ipv4ll_set_mac(ll, ha) >= 0); + assert_se(sd_ipv4ll_set_callback(ll, ll_handler, NULL) >= 0); + + if (seed_str) { + unsigned seed; + + assert_se(safe_atou(seed_str, &seed) >= 0); + + assert_se(sd_ipv4ll_set_address_seed(ll, seed) >= 0); + } + + if (start_address && in4_addr_is_set(start_address)) + assert_se(sd_ipv4ll_set_address(ll, start_address) >= 0); + + log_info("starting IPv4LL client"); + + assert_se(sd_ipv4ll_start(ll) >= 0); + + assert_se(sd_event_loop(e) >= 0); + + assert_se(!sd_ipv4ll_unref(ll)); + + return EXIT_SUCCESS; +} + +static int test_ll(const char *ifname, const char *seed, const struct in_addr *start_address) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL, *reply = NULL; + struct ether_addr ha; + int ifindex; + + assert_se(sd_event_new(&e) >= 0); + + assert_se(sd_netlink_open(&rtnl) >= 0); + assert_se(sd_netlink_attach_event(rtnl, e, 0) >= 0); + + assert_se(sd_rtnl_message_new_link(rtnl, &m, RTM_GETLINK, 0) >= 0); + assert_se(sd_netlink_message_append_string(m, IFLA_IFNAME, ifname) >= 0); + assert_se(sd_netlink_call(rtnl, m, 0, &reply) >= 0); + + assert_se(sd_rtnl_message_link_get_ifindex(reply, &ifindex) >= 0); + assert_se(sd_netlink_message_read_ether_addr(reply, IFLA_ADDRESS, &ha) >= 0); + + client_run(ifindex, seed, start_address, &ha, e); + + return EXIT_SUCCESS; +} + +int main(int argc, char *argv[]) { + test_setup_logging(LOG_DEBUG); + + if (argc == 2) + return test_ll(argv[1], NULL, NULL); + else if (argc == 3) { + int r; + union in_addr_union a; + + r = in_addr_from_string(AF_INET, argv[2], &a); + if (r < 0) + return test_ll(argv[1], argv[2], NULL); + else + return test_ll(argv[1], NULL, &a.in); + } else { + log_error("This program takes one or two arguments.\n" + "\t %s <ifname> [<seed>|<start_address>]", program_invocation_short_name); + return EXIT_FAILURE; + } +} diff --git a/src/libsystemd-network/test-ipv4ll.c b/src/libsystemd-network/test-ipv4ll.c new file mode 100644 index 0000000..bb42930 --- /dev/null +++ b/src/libsystemd-network/test-ipv4ll.c @@ -0,0 +1,206 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Axis Communications AB. All rights reserved. +***/ + +#include <errno.h> +#include <netinet/if_ether.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <unistd.h> + +#include "sd-ipv4ll.h" + +#include "arp-util.h" +#include "fd-util.h" +#include "socket-util.h" +#include "tests.h" + +static bool verbose = false; +static bool extended = false; +static int test_fd[2]; + +static int basic_request_handler_bind = 0; +static int basic_request_handler_stop = 0; +static void* basic_request_handler_userdata = (void*) 0xCABCAB; + +static void basic_request_handler(sd_ipv4ll *ll, int event, void *userdata) { + assert_se(userdata == basic_request_handler_userdata); + + switch (event) { + case SD_IPV4LL_EVENT_STOP: + basic_request_handler_stop = 1; + break; + case SD_IPV4LL_EVENT_BIND: + basic_request_handler_bind = 1; + break; + default: + assert_se(0); + break; + } +} + +int arp_send_packet( + int fd, + int ifindex, + const struct in_addr *pa, + const struct ether_addr *ha, + bool announce) { + + struct ether_arp ea = {}; + + assert_se(fd >= 0); + assert_se(ifindex > 0); + assert_se(pa); + assert_se(ha); + + if (send(fd, &ea, sizeof(struct ether_arp), 0) < 0) + return -errno; + + return 0; +} + +int arp_update_filter(int fd, const struct in_addr *a, const struct ether_addr *eth_mac) { + return 0; +} + +int arp_network_bind_raw_socket(int ifindex, const struct in_addr *a, const struct ether_addr *eth_mac) { + if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_fd) < 0) + return -errno; + + return test_fd[0]; +} + +static void test_public_api_setters(sd_event *e) { + struct in_addr address = {}; + uint64_t seed = 0; + sd_ipv4ll *ll; + struct ether_addr mac_addr = { + .ether_addr_octet = {'A', 'B', 'C', '1', '2', '3'}}; + + if (verbose) + printf("* %s\n", __func__); + + assert_se(sd_ipv4ll_new(&ll) == 0); + assert_se(ll); + + assert_se(sd_ipv4ll_attach_event(NULL, NULL, 0) == -EINVAL); + assert_se(sd_ipv4ll_attach_event(ll, e, 0) == 0); + assert_se(sd_ipv4ll_attach_event(ll, e, 0) == -EBUSY); + + assert_se(sd_ipv4ll_set_callback(NULL, NULL, NULL) == -EINVAL); + assert_se(sd_ipv4ll_set_callback(ll, NULL, NULL) == 0); + + assert_se(sd_ipv4ll_set_address(ll, &address) == -EINVAL); + address.s_addr |= htobe32(169U << 24 | 254U << 16); + assert_se(sd_ipv4ll_set_address(ll, &address) == -EINVAL); + address.s_addr |= htobe32(0x00FF); + assert_se(sd_ipv4ll_set_address(ll, &address) == -EINVAL); + address.s_addr |= htobe32(0xF000); + assert_se(sd_ipv4ll_set_address(ll, &address) == 0); + address.s_addr |= htobe32(0x0F00); + assert_se(sd_ipv4ll_set_address(ll, &address) == -EINVAL); + + assert_se(sd_ipv4ll_set_address_seed(NULL, seed) == -EINVAL); + assert_se(sd_ipv4ll_set_address_seed(ll, seed) == 0); + + assert_se(sd_ipv4ll_set_mac(NULL, NULL) == -EINVAL); + assert_se(sd_ipv4ll_set_mac(ll, NULL) == -EINVAL); + assert_se(sd_ipv4ll_set_mac(ll, &mac_addr) == 0); + + assert_se(sd_ipv4ll_set_ifindex(NULL, -1) == -EINVAL); + assert_se(sd_ipv4ll_set_ifindex(ll, -1) == -EINVAL); + assert_se(sd_ipv4ll_set_ifindex(ll, -99) == -EINVAL); + assert_se(sd_ipv4ll_set_ifindex(ll, 1) == 0); + + assert_se(sd_ipv4ll_ref(ll) == ll); + assert_se(sd_ipv4ll_unref(ll) == NULL); + + /* Cleanup */ + assert_se(sd_ipv4ll_unref(ll) == NULL); +} + +static void test_basic_request(sd_event *e, const struct in_addr *start_address) { + + sd_ipv4ll *ll; + struct ether_arp arp; + struct ether_addr mac_addr = { + .ether_addr_octet = {'A', 'B', 'C', '1', '2', '3'}}; + + if (verbose) + printf("* %s\n", __func__); + + assert_se(sd_ipv4ll_new(&ll) == 0); + if (in4_addr_is_set(start_address)) + assert_se(sd_ipv4ll_set_address(ll, start_address) >= 0); + assert_se(sd_ipv4ll_start(ll) == -EINVAL); + + assert_se(sd_ipv4ll_attach_event(ll, e, 0) == 0); + assert_se(sd_ipv4ll_start(ll) == -EINVAL); + + assert_se(sd_ipv4ll_set_mac(ll, &mac_addr) == 0); + assert_se(sd_ipv4ll_start(ll) == -EINVAL); + + assert_se(sd_ipv4ll_set_callback(ll, basic_request_handler, + basic_request_handler_userdata) == 0); + assert_se(sd_ipv4ll_start(ll) == -EINVAL); + + assert_se(sd_ipv4ll_set_ifindex(ll, 1) == 0); + assert_se(sd_ipv4ll_start(ll) == 1); + + sd_event_run(e, UINT64_MAX); + assert_se(sd_ipv4ll_start(ll) == 0); + + assert_se(sd_ipv4ll_is_running(ll)); + + /* PROBE */ + sd_event_run(e, UINT64_MAX); + assert_se(recv(test_fd[1], &arp, sizeof(struct ether_arp), 0) == sizeof(struct ether_arp)); + + if (extended) { + /* PROBE */ + sd_event_run(e, UINT64_MAX); + assert_se(recv(test_fd[1], &arp, sizeof(struct ether_arp), 0) == sizeof(struct ether_arp)); + + /* PROBE */ + sd_event_run(e, UINT64_MAX); + assert_se(recv(test_fd[1], &arp, sizeof(struct ether_arp), 0) == sizeof(struct ether_arp)); + + sd_event_run(e, UINT64_MAX); + assert_se(basic_request_handler_bind == 1); + + if (in4_addr_is_set(start_address)) { + struct in_addr address; + + assert_se(sd_ipv4ll_get_address(ll, &address) >= 0); + assert_se(start_address->s_addr == address.s_addr); + } + } + + sd_ipv4ll_stop(ll); + assert_se(basic_request_handler_stop == 1); + + /* Cleanup */ + assert_se(sd_ipv4ll_unref(ll) == NULL); + safe_close(test_fd[1]); +} + +int main(int argc, char *argv[]) { + struct in_addr start_address = {}; + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + + test_setup_logging(LOG_DEBUG); + + assert_se(sd_event_new(&e) >= 0); + + test_public_api_setters(e); + test_basic_request(e, &start_address); + + basic_request_handler_bind = 0; + basic_request_handler_stop = 0; + start_address.s_addr = htobe32(169U << 24 | 254U << 16 | 1U << 8 | 2U); + test_basic_request(e, &start_address); + + return 0; +} diff --git a/src/libsystemd-network/test-lldp-rx.c b/src/libsystemd-network/test-lldp-rx.c new file mode 100644 index 0000000..feb53b5 --- /dev/null +++ b/src/libsystemd-network/test-lldp-rx.c @@ -0,0 +1,378 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <errno.h> +#include <net/ethernet.h> +#include <stdio.h> +#include <unistd.h> + +#include "sd-event.h" +#include "sd-lldp-rx.h" + +#include "alloc-util.h" +#include "fd-util.h" +#include "lldp-network.h" +#include "macro.h" +#include "string-util.h" +#include "tests.h" + +#define TEST_LLDP_PORT "em1" +#define TEST_LLDP_TYPE_SYSTEM_NAME "systemd-lldp" +#define TEST_LLDP_TYPE_SYSTEM_DESC "systemd-lldp-desc" + +static int test_fd[2] = EBADF_PAIR; +static int lldp_rx_handler_calls; + +int lldp_network_bind_raw_socket(int ifindex) { + if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_fd) < 0) + return -errno; + + return test_fd[0]; +} + +static void lldp_rx_handler(sd_lldp_rx *lldp_rx, sd_lldp_rx_event_t event, sd_lldp_neighbor *n, void *userdata) { + lldp_rx_handler_calls++; +} + +static int start_lldp_rx(sd_lldp_rx **lldp_rx, sd_event *e, sd_lldp_rx_callback_t cb, void *cb_data) { + int r; + + r = sd_lldp_rx_new(lldp_rx); + if (r < 0) + return r; + + r = sd_lldp_rx_set_ifindex(*lldp_rx, 42); + if (r < 0) + return r; + + r = sd_lldp_rx_set_callback(*lldp_rx, cb, cb_data); + if (r < 0) + return r; + + r = sd_lldp_rx_attach_event(*lldp_rx, e, 0); + if (r < 0) + return r; + + r = sd_lldp_rx_start(*lldp_rx); + if (r < 0) + return r; + + return 0; +} + +static int stop_lldp_rx(sd_lldp_rx *lldp_rx) { + int r; + + r = sd_lldp_rx_stop(lldp_rx); + if (r < 0) + return r; + + r = sd_lldp_rx_detach_event(lldp_rx); + if (r < 0) + return r; + + sd_lldp_rx_unref(lldp_rx); + safe_close(test_fd[1]); + + return 0; +} + +static void test_receive_basic_packet(sd_event *e) { + + static const uint8_t frame[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x07, 0x04, 0x00, 0x01, 0x02, /* Chassis: MAC, 00:01:02:03:04:05 */ + 0x03, 0x04, 0x05, + 0x04, 0x04, 0x05, 0x31, 0x2f, 0x33, /* Port: interface name, "1/3" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + /* LLDP optional TLVs */ + 0x08, 0x04, 0x50, 0x6f, 0x72, 0x74, /* Port Description: "Port" */ + 0x0a, 0x03, 0x53, 0x59, 0x53, /* System Name: "SYS" */ + 0x0c, 0x04, 0x66, 0x6f, 0x6f, 0x00, /* System Description: "foo" (NULL-terminated) */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + + sd_lldp_rx *lldp_rx; + sd_lldp_neighbor **neighbors; + uint8_t type; + const void *data; + uint16_t ttl; + size_t length; + const char *str; + + lldp_rx_handler_calls = 0; + assert_se(start_lldp_rx(&lldp_rx, e, lldp_rx_handler, NULL) == 0); + + assert_se(write(test_fd[1], frame, sizeof(frame)) == sizeof(frame)); + sd_event_run(e, 0); + assert_se(lldp_rx_handler_calls == 1); + assert_se(sd_lldp_rx_get_neighbors(lldp_rx, &neighbors) == 1); + + assert_se(sd_lldp_neighbor_get_chassis_id(neighbors[0], &type, &data, &length) == 0); + assert_se(type == SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS); + assert_se(length == ETH_ALEN); + assert_se(!memcmp(data, "\x00\x01\x02\x03\x04\x05", ETH_ALEN)); + + assert_se(sd_lldp_neighbor_get_port_id(neighbors[0], &type, &data, &length) == 0); + assert_se(type == SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME); + assert_se(length == 3); + assert_se(!memcmp(data, "1/3", 3)); + + assert_se(sd_lldp_neighbor_get_port_description(neighbors[0], &str) == 0); + assert_se(streq(str, "Port")); + + assert_se(sd_lldp_neighbor_get_system_name(neighbors[0], &str) == 0); + assert_se(streq(str, "SYS")); + + assert_se(sd_lldp_neighbor_get_system_description(neighbors[0], &str) == 0); + assert_se(streq(str, "foo")); + + assert_se(sd_lldp_neighbor_get_ttl(neighbors[0], &ttl) == 0); + assert_se(ttl == 120); + + sd_lldp_neighbor_unref(neighbors[0]); + free(neighbors); + + assert_se(stop_lldp_rx(lldp_rx) == 0); +} + +static void test_receive_incomplete_packet(sd_event *e) { + sd_lldp_rx *lldp_rx; + sd_lldp_neighbor **neighbors; + uint8_t frame[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x07, 0x04, 0x00, 0x01, 0x02, /* Chassis: MAC, 00:01:02:03:04:05 */ + 0x03, 0x04, 0x05, + 0x04, 0x04, 0x05, 0x31, 0x2f, 0x33, /* Port: interface name, "1/3" */ + /* Missing TTL */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + + lldp_rx_handler_calls = 0; + assert_se(start_lldp_rx(&lldp_rx, e, lldp_rx_handler, NULL) == 0); + + assert_se(write(test_fd[1], frame, sizeof(frame)) == sizeof(frame)); + sd_event_run(e, 0); + assert_se(lldp_rx_handler_calls == 0); + assert_se(sd_lldp_rx_get_neighbors(lldp_rx, &neighbors) == 0); + + assert_se(stop_lldp_rx(lldp_rx) == 0); +} + +static void test_receive_oui_packet(sd_event *e) { + sd_lldp_rx *lldp_rx; + sd_lldp_neighbor **neighbors; + uint8_t frame[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x07, 0x04, 0x00, 0x01, 0x02, /* Chassis: MAC, 00:01:02:03:04:05 */ + 0x03, 0x04, 0x05, + 0x04, 0x04, 0x05, 0x31, 0x2f, 0x33, /* Port TLV: interface name, "1/3" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + /* LLDP optional TLVs */ + 0xfe, 0x06, 0x00, 0x80, 0xc2, 0x01, /* Port VLAN ID: 0x1234 */ + 0x12, 0x34, + 0xfe, 0x07, 0x00, 0x80, 0xc2, 0x02, /* Port and protocol: flag 1, PPVID 0x7788 */ + 0x01, 0x77, 0x88, + 0xfe, 0x0d, 0x00, 0x80, 0xc2, 0x03, /* VLAN Name: ID 0x1234, name "Vlan51" */ + 0x12, 0x34, 0x06, 0x56, 0x6c, 0x61, + 0x6e, 0x35, 0x31, + 0xfe, 0x06, 0x00, 0x80, 0xc2, 0x06, /* Management VID: 0x0102 */ + 0x01, 0x02, + 0xfe, 0x09, 0x00, 0x80, 0xc2, 0x07, /* Link aggregation: status 1, ID 0x00140012 */ + 0x01, 0x00, 0x14, 0x00, 0x12, + 0xfe, 0x07, 0x00, 0x12, 0x0f, 0x02, /* 802.3 Power via MDI: PSE, MDI enabled */ + 0x07, 0x01, 0x00, + 0x00, 0x00 /* End of LLDPDU */ + }; + + lldp_rx_handler_calls = 0; + assert_se(start_lldp_rx(&lldp_rx, e, lldp_rx_handler, NULL) == 0); + + assert_se(write(test_fd[1], frame, sizeof(frame)) == sizeof(frame)); + sd_event_run(e, 0); + assert_se(lldp_rx_handler_calls == 1); + assert_se(sd_lldp_rx_get_neighbors(lldp_rx, &neighbors) == 1); + + assert_se(sd_lldp_neighbor_tlv_rewind(neighbors[0]) >= 0); + assert_se(sd_lldp_neighbor_tlv_is_type(neighbors[0], SD_LLDP_TYPE_CHASSIS_ID) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_type(neighbors[0], SD_LLDP_TYPE_PORT_ID) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_type(neighbors[0], SD_LLDP_TYPE_TTL) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_oui(neighbors[0], SD_LLDP_OUI_802_1, SD_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_oui(neighbors[0], SD_LLDP_OUI_802_1, SD_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_oui(neighbors[0], SD_LLDP_OUI_802_1, SD_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_oui(neighbors[0], SD_LLDP_OUI_802_1, SD_LLDP_OUI_802_1_SUBTYPE_MANAGEMENT_VID) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_oui(neighbors[0], SD_LLDP_OUI_802_1, SD_LLDP_OUI_802_1_SUBTYPE_LINK_AGGREGATION) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_oui(neighbors[0], SD_LLDP_OUI_802_3, SD_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) > 0); + assert_se(sd_lldp_neighbor_tlv_is_type(neighbors[0], SD_LLDP_TYPE_END) > 0); + assert_se(sd_lldp_neighbor_tlv_next(neighbors[0]) == 0); + + sd_lldp_neighbor_unref(neighbors[0]); + free(neighbors); + + assert_se(stop_lldp_rx(lldp_rx) == 0); +} + +static void test_multiple_neighbors_sorted(sd_event *e) { + + static const uint8_t frame1[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x04, 0x01, '1', '/', '2', /* Chassis component: "1/2" */ + 0x04, 0x04, 0x02, '2', '/', '3', /* Port component: "2/3" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + static const uint8_t frame2[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x04, 0x01, '2', '/', '1', /* Chassis component: "2/1" */ + 0x04, 0x04, 0x02, '1', '/', '3', /* Port component: "1/3" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + static const uint8_t frame3[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x05, 0x01, '2', '/', '1', '0', /* Chassis component: "2/10" */ + 0x04, 0x04, 0x02, '1', '/', '0', /* Port component: "1/0" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + static const uint8_t frame4[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x05, 0x01, '2', '/', '1', '9', /* Chassis component: "2/19" */ + 0x04, 0x04, 0x02, '1', '/', '0', /* Port component: "1/0" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + static const uint8_t frame5[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x04, 0x01, '1', '/', '2', /* Chassis component: "1/2" */ + 0x04, 0x05, 0x02, '2', '/', '1', '0', /* Port component: "2/10" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + static const uint8_t frame6[] = { + /* Ethernet header */ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, /* Destination MAC */ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* Source MAC */ + 0x88, 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, 0x04, 0x01, '1', '/', '2', /* Chassis component: "1/2" */ + 0x04, 0x05, 0x02, '2', '/', '3', '9', /* Port component: "2/10" */ + 0x06, 0x02, 0x00, 0x78, /* TTL: 120 seconds */ + 0x00, 0x00 /* End Of LLDPDU */ + }; + static const char* expected[] = { + /* ordered pairs of Chassis+Port */ + "1/2", "2/10", + "1/2", "2/3", + "1/2", "2/39", + "2/1", "1/3", + "2/10", "1/0", + "2/19", "1/0", + }; + + sd_lldp_rx *lldp_rx; + sd_lldp_neighbor **neighbors; + int i; + uint8_t type; + const void *data; + size_t length, expected_length; + uint16_t ttl; + + lldp_rx_handler_calls = 0; + assert_se(start_lldp_rx(&lldp_rx, e, lldp_rx_handler, NULL) == 0); + + assert_se(write(test_fd[1], frame1, sizeof(frame1)) == sizeof(frame1)); + sd_event_run(e, 0); + assert_se(write(test_fd[1], frame2, sizeof(frame2)) == sizeof(frame2)); + sd_event_run(e, 0); + assert_se(write(test_fd[1], frame3, sizeof(frame3)) == sizeof(frame3)); + sd_event_run(e, 0); + assert_se(write(test_fd[1], frame4, sizeof(frame4)) == sizeof(frame4)); + sd_event_run(e, 0); + assert_se(write(test_fd[1], frame5, sizeof(frame5)) == sizeof(frame5)); + sd_event_run(e, 0); + assert_se(write(test_fd[1], frame6, sizeof(frame6)) == sizeof(frame6)); + sd_event_run(e, 0); + assert_se(lldp_rx_handler_calls == 6); + + assert_se(sd_lldp_rx_get_neighbors(lldp_rx, &neighbors) == 6); + + for (i = 0; i < 6; i++) { + assert_se(sd_lldp_neighbor_get_chassis_id(neighbors[i], &type, &data, &length) == 0); + assert_se(type == SD_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT); + expected_length = strlen(expected[2 * i]); + assert_se(length == expected_length); + assert_se(memcmp(data, expected[2 * i], expected_length) == 0); + + assert_se(sd_lldp_neighbor_get_port_id(neighbors[i], &type, &data, &length) == 0); + assert_se(type == SD_LLDP_PORT_SUBTYPE_PORT_COMPONENT); + expected_length = strlen(expected[2 * i + 1]); + assert_se(length == expected_length); + assert_se(memcmp(data, expected[2 * i + 1], expected_length) == 0); + + assert_se(sd_lldp_neighbor_get_ttl(neighbors[i], &ttl) == 0); + assert_se(ttl == 120); + } + + for (i = 0; i < 6; i++) + sd_lldp_neighbor_unref(neighbors[i]); + free(neighbors); + + assert_se(stop_lldp_rx(lldp_rx) == 0); +} + +int main(int argc, char *argv[]) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + + test_setup_logging(LOG_DEBUG); + + /* LLDP reception tests */ + assert_se(sd_event_new(&e) == 0); + test_receive_basic_packet(e); + test_receive_incomplete_packet(e); + test_receive_oui_packet(e); + test_multiple_neighbors_sorted(e); + + return 0; +} diff --git a/src/libsystemd-network/test-ndisc-ra.c b/src/libsystemd-network/test-ndisc-ra.c new file mode 100644 index 0000000..23abe78 --- /dev/null +++ b/src/libsystemd-network/test-ndisc-ra.c @@ -0,0 +1,376 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2017 Intel Corporation. All rights reserved. +***/ + +#include <netinet/icmp6.h> +#include <arpa/inet.h> +#include <unistd.h> + +#include "sd-radv.h" + +#include "alloc-util.h" +#include "hexdecoct.h" +#include "icmp6-util-unix.h" +#include "socket-util.h" +#include "strv.h" +#include "tests.h" + +static struct ether_addr mac_addr = { + .ether_addr_octet = { 0x78, 0x2b, 0xcb, 0xb3, 0x6d, 0x53 } +}; + +static uint8_t advertisement[] = { + /* ICMPv6 Router Advertisement, no checksum */ + 0x86, 0x00, 0x00, 0x00, 0x40, 0xc0, 0x00, 0xb4, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* Source Link Layer Address Option */ + 0x01, 0x01, 0x78, 0x2b, 0xcb, 0xb3, 0x6d, 0x53, + /* Prefix Information Option */ + 0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x01, 0xf4, + 0x00, 0x00, 0x01, 0xb8, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* Prefix Information Option */ + 0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x0e, 0x10, + 0x00, 0x00, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x01, 0x0d, 0xb8, 0x0b, 0x16, 0xd0, 0x0d, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* Prefix Information Option */ + 0x03, 0x04, 0x30, 0xc0, 0x00, 0x00, 0x0e, 0x10, + 0x00, 0x00, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x01, 0x0d, 0xb8, 0xc0, 0x01, 0x0d, 0xad, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* Recursive DNS Server Option */ + 0x19, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + /* DNS Search List Option */ + 0x1f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, + 0x03, 0x6c, 0x61, 0x62, 0x05, 0x69, 0x6e, 0x74, + 0x72, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static bool test_stopped; +static struct { + struct in6_addr address; + unsigned char prefixlen; + uint32_t valid; + uint32_t preferred; + bool successful; +} prefix[] = { + { { { { 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, 64, + 500, 440, true }, + { { { { 0x20, 0x01, 0x0d, 0xb8, 0x0b, 0x16, 0xd0, 0x0d, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, 64, + /* indicate default valid and preferred lifetimes for the test code */ + 0, 0, true }, + { { { { 0x20, 0x01, 0x0d, 0xb8, 0x0b, 0x16, 0xd0, 0x0d, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, 58, + 0, 0, + /* indicate that this prefix already exists */ + false }, + { { { { 0x20, 0x01, 0x0d, 0xb8, 0x0b, 0x16, 0xd0, 0x0d, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, 120, + 0, 0, + /* indicate that this prefix already exists */ + false }, + { { { { 0x20, 0x01, 0x0d, 0xb8, 0x0b, 0x16, 0xd0, 0x0d, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, 12, + 0, 0, + /* indicate that this prefix already exists */ + false }, + { { { { 0x20, 0x01, 0x0d, 0xb8, 0xc0, 0x01, 0x0d, 0xad, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, 48, + 0, 0, true }, + { { { { 0x20, 0x01, 0x0d, 0xb8, 0xc0, 0x01, 0x0d, 0xad, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, 60, + 0, 0, + /* indicate that this prefix already exists */ + false }, +}; + +static const struct in6_addr test_rdnss = { { { 0x20, 0x01, 0x0d, 0xb8, + 0xde, 0xad, 0xbe, 0xef, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01 } } }; +static const char *test_dnssl[] = { "lab.intra", + NULL }; + +TEST(radv_prefix) { + sd_radv_prefix *p; + + assert_se(sd_radv_prefix_new(&p) >= 0); + + assert_se(sd_radv_prefix_set_onlink(NULL, true) < 0); + assert_se(sd_radv_prefix_set_onlink(p, true) >= 0); + assert_se(sd_radv_prefix_set_onlink(p, false) >= 0); + + assert_se(sd_radv_prefix_set_address_autoconfiguration(NULL, true) < 0); + assert_se(sd_radv_prefix_set_address_autoconfiguration(p, true) >= 0); + assert_se(sd_radv_prefix_set_address_autoconfiguration(p, false) >= 0); + + assert_se(sd_radv_prefix_set_valid_lifetime(NULL, 1, 1) < 0); + assert_se(sd_radv_prefix_set_valid_lifetime(p, 0, 0) >= 0); + assert_se(sd_radv_prefix_set_valid_lifetime(p, 300 * USEC_PER_SEC, USEC_INFINITY) >= 0); + assert_se(sd_radv_prefix_set_valid_lifetime(p, 300 * USEC_PER_SEC, USEC_PER_YEAR) >= 0); + + assert_se(sd_radv_prefix_set_preferred_lifetime(NULL, 1, 1) < 0); + assert_se(sd_radv_prefix_set_preferred_lifetime(p, 0, 0) >= 0); + assert_se(sd_radv_prefix_set_preferred_lifetime(p, 300 * USEC_PER_SEC, USEC_INFINITY) >= 0); + assert_se(sd_radv_prefix_set_preferred_lifetime(p, 300 * USEC_PER_SEC, USEC_PER_YEAR) >= 0); + + assert_se(sd_radv_prefix_set_prefix(NULL, NULL, 0) < 0); + assert_se(sd_radv_prefix_set_prefix(p, NULL, 0) < 0); + + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 64) >= 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 0) < 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 1) < 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 2) < 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 3) >= 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 125) >= 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 128) >= 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 129) < 0); + assert_se(sd_radv_prefix_set_prefix(p, &prefix[0].address, 255) < 0); + + assert_se(!sd_radv_prefix_unref(p)); +} + +TEST(radv_route_prefix) { + sd_radv_route_prefix *p; + + assert_se(sd_radv_route_prefix_new(&p) >= 0); + + assert_se(sd_radv_route_prefix_set_lifetime(NULL, 1, 1) < 0); + assert_se(sd_radv_route_prefix_set_lifetime(p, 0, 0) >= 0); + assert_se(sd_radv_route_prefix_set_lifetime(p, 300 * USEC_PER_SEC, USEC_INFINITY) >= 0); + assert_se(sd_radv_route_prefix_set_lifetime(p, 300 * USEC_PER_SEC, USEC_PER_YEAR) >= 0); + + assert_se(sd_radv_route_prefix_set_prefix(NULL, NULL, 0) < 0); + assert_se(sd_radv_route_prefix_set_prefix(p, NULL, 0) < 0); + + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 64) >= 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 0) >= 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 1) >= 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 2) >= 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 3) >= 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 125) >= 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 128) >= 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 129) < 0); + assert_se(sd_radv_route_prefix_set_prefix(p, &prefix[0].address, 255) < 0); + + assert_se(!sd_radv_route_prefix_unref(p)); +} + +TEST(radv_pref64_prefix) { + sd_radv_pref64_prefix *p; + + assert_se(sd_radv_pref64_prefix_new(&p) >= 0); + + assert_se(sd_radv_pref64_prefix_set_prefix(NULL, NULL, 0, 0) < 0); + assert_se(sd_radv_pref64_prefix_set_prefix(p, NULL, 0, 0) < 0); + + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 32, 300 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 40, 300 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 48, 300 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 56, 300 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 64, 300 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 96, 300 * USEC_PER_SEC) >= 0); + + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 80, 300 * USEC_PER_SEC) < 0); + assert_se(sd_radv_pref64_prefix_set_prefix(p, &prefix[0].address, 80, USEC_PER_DAY) < 0); + + assert_se(!sd_radv_pref64_prefix_unref(p)); +} + +TEST(radv) { + sd_radv *ra; + + assert_se(sd_radv_new(&ra) >= 0); + assert_se(ra); + + assert_se(sd_radv_set_ifindex(NULL, 0) < 0); + assert_se(sd_radv_set_ifindex(ra, 0) < 0); + assert_se(sd_radv_set_ifindex(ra, -1) < 0); + assert_se(sd_radv_set_ifindex(ra, -2) < 0); + assert_se(sd_radv_set_ifindex(ra, 42) >= 0); + + assert_se(sd_radv_set_mac(NULL, NULL) < 0); + assert_se(sd_radv_set_mac(ra, NULL) >= 0); + assert_se(sd_radv_set_mac(ra, &mac_addr) >= 0); + + assert_se(sd_radv_set_mtu(NULL, 0) < 0); + assert_se(sd_radv_set_mtu(ra, 0) < 0); + assert_se(sd_radv_set_mtu(ra, 1279) < 0); + assert_se(sd_radv_set_mtu(ra, 1280) >= 0); + assert_se(sd_radv_set_mtu(ra, ~0) >= 0); + + assert_se(sd_radv_set_hop_limit(NULL, 0) < 0); + assert_se(sd_radv_set_hop_limit(ra, 0) >= 0); + assert_se(sd_radv_set_hop_limit(ra, ~0) >= 0); + + assert_se(sd_radv_set_router_lifetime(NULL, 0) < 0); + assert_se(sd_radv_set_router_lifetime(ra, 0) >= 0); + assert_se(sd_radv_set_router_lifetime(ra, USEC_INFINITY) < 0); + assert_se(sd_radv_set_router_lifetime(ra, USEC_PER_YEAR) < 0); + assert_se(sd_radv_set_router_lifetime(ra, 300 * USEC_PER_SEC) >= 0); + + assert_se(sd_radv_set_preference(NULL, 0) < 0); + assert_se(sd_radv_set_preference(ra, SD_NDISC_PREFERENCE_LOW) >= 0); + assert_se(sd_radv_set_preference(ra, SD_NDISC_PREFERENCE_MEDIUM) >= 0); + assert_se(sd_radv_set_preference(ra, SD_NDISC_PREFERENCE_HIGH) >= 0); + assert_se(sd_radv_set_preference(ra, ~0) < 0); + + assert_se(sd_radv_set_preference(ra, SD_NDISC_PREFERENCE_HIGH) >= 0); + assert_se(sd_radv_set_router_lifetime(ra, 300 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_set_router_lifetime(ra, 0) < 0); + assert_se(sd_radv_set_preference(ra, SD_NDISC_PREFERENCE_MEDIUM) >= 0); + assert_se(sd_radv_set_router_lifetime(ra, 0) >= 0); + + assert_se(sd_radv_set_managed_information(NULL, true) < 0); + assert_se(sd_radv_set_managed_information(ra, true) >= 0); + assert_se(sd_radv_set_managed_information(ra, false) >= 0); + + assert_se(sd_radv_set_other_information(NULL, true) < 0); + assert_se(sd_radv_set_other_information(ra, true) >= 0); + assert_se(sd_radv_set_other_information(ra, false) >= 0); + + assert_se(sd_radv_set_retransmit(NULL, 10 * USEC_PER_MSEC) < 0); + assert_se(sd_radv_set_retransmit(ra, 10 * USEC_PER_MSEC) >= 0); + assert_se(sd_radv_set_retransmit(ra, 0) >= 0); + assert_se(sd_radv_set_retransmit(ra, usec_add(UINT32_MAX * USEC_PER_MSEC, USEC_PER_MSEC)) < 0); + + assert_se(sd_radv_set_rdnss(NULL, 0, NULL, 0) < 0); + assert_se(sd_radv_set_rdnss(ra, 0, NULL, 0) >= 0); + assert_se(sd_radv_set_rdnss(ra, 0, NULL, 128) < 0); + assert_se(sd_radv_set_rdnss(ra, 600 * USEC_PER_SEC, &test_rdnss, 0) >= 0); + assert_se(sd_radv_set_rdnss(ra, 600 * USEC_PER_SEC, &test_rdnss, 1) >= 0); + assert_se(sd_radv_set_rdnss(ra, 0, &test_rdnss, 1) >= 0); + assert_se(sd_radv_set_rdnss(ra, 0, NULL, 0) >= 0); + + assert_se(sd_radv_set_dnssl(ra, 0, NULL) >= 0); + assert_se(sd_radv_set_dnssl(ra, 600 * USEC_PER_SEC, NULL) >= 0); + assert_se(sd_radv_set_dnssl(ra, 0, (char **)test_dnssl) >= 0); + assert_se(sd_radv_set_dnssl(ra, 600 * USEC_PER_SEC, (char **)test_dnssl) >= 0); + + assert_se(sd_radv_set_home_agent_information(NULL, true) < 0); + assert_se(sd_radv_set_home_agent_information(ra, true) >= 0); + assert_se(sd_radv_set_home_agent_information(ra, false) >= 0); + + assert_se(sd_radv_set_home_agent_preference(NULL, 10) < 0); + assert_se(sd_radv_set_home_agent_preference(ra, 10) >= 0); + assert_se(sd_radv_set_home_agent_preference(ra, 0) >= 0); + + assert_se(sd_radv_set_home_agent_lifetime(NULL, 300 * USEC_PER_SEC) < 0); + assert_se(sd_radv_set_home_agent_lifetime(ra, 300 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_set_home_agent_lifetime(ra, 0) >= 0); + assert_se(sd_radv_set_home_agent_lifetime(ra, USEC_PER_DAY) < 0); + + ra = sd_radv_unref(ra); + assert_se(!ra); +} + +static int radv_recv(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + sd_radv *ra = userdata; + unsigned char buf[168]; + size_t i; + + assert_se(read(test_fd[0], &buf, sizeof(buf)) == sizeof(buf)); + + /* router lifetime must be zero when test is stopped */ + if (test_stopped) { + advertisement[6] = 0x00; + advertisement[7] = 0x00; + } + + printf ("Received Router Advertisement with lifetime %i\n", + (advertisement[6] << 8) + advertisement[7]); + + /* test only up to buf size, rest is not yet implemented */ + for (i = 0; i < sizeof(buf); i++) { + if (!(i % 8)) + printf("%3zu: ", i); + + printf("0x%02x", buf[i]); + + assert_se(buf[i] == advertisement[i]); + + if ((i + 1) % 8) + printf(", "); + else + printf("\n"); + } + + if (test_stopped) { + sd_event *e; + + e = sd_radv_get_event(ra); + sd_event_exit(e, 0); + + return 0; + } + + assert_se(sd_radv_stop(ra) >= 0); + test_stopped = true; + + return 0; +} + +TEST(ra) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *recv_router_advertisement = NULL; + _cleanup_(sd_radv_unrefp) sd_radv *ra = NULL; + + assert_se(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, test_fd) >= 0); + + assert_se(sd_event_new(&e) >= 0); + + assert_se(sd_radv_new(&ra) >= 0); + assert_se(ra); + + assert_se(sd_radv_attach_event(ra, e, 0) >= 0); + + assert_se(sd_radv_set_ifindex(ra, 42) >= 0); + assert_se(sd_radv_set_mac(ra, &mac_addr) >= 0); + assert_se(sd_radv_set_router_lifetime(ra, 180 * USEC_PER_SEC) >= 0); + assert_se(sd_radv_set_hop_limit(ra, 64) >= 0); + assert_se(sd_radv_set_managed_information(ra, true) >= 0); + assert_se(sd_radv_set_other_information(ra, true) >= 0); + assert_se(sd_radv_set_rdnss(ra, 60 * USEC_PER_SEC, &test_rdnss, 1) >= 0); + assert_se(sd_radv_set_dnssl(ra, 60 * USEC_PER_SEC, (char **)test_dnssl) >= 0); + + for (unsigned i = 0; i < ELEMENTSOF(prefix); i++) { + sd_radv_prefix *p; + + printf("Test prefix %u\n", i); + assert_se(sd_radv_prefix_new(&p) >= 0); + + assert_se(sd_radv_prefix_set_prefix(p, &prefix[i].address, + prefix[i].prefixlen) >= 0); + if (prefix[i].valid > 0) + assert_se(sd_radv_prefix_set_valid_lifetime(p, prefix[i].valid * USEC_PER_SEC, USEC_INFINITY) >= 0); + if (prefix[i].preferred > 0) + assert_se(sd_radv_prefix_set_preferred_lifetime(p, prefix[i].preferred * USEC_PER_SEC, USEC_INFINITY) >= 0); + + assert_se((sd_radv_add_prefix(ra, p) >= 0) == prefix[i].successful); + /* If the previous sd_radv_add_prefix() succeeds, then also the second call should also succeed. */ + assert_se((sd_radv_add_prefix(ra, p) >= 0) == prefix[i].successful); + + p = sd_radv_prefix_unref(p); + assert_se(!p); + } + + assert_se(sd_event_add_io(e, &recv_router_advertisement, test_fd[0], EPOLLIN, radv_recv, ra) >= 0); + assert_se(sd_event_source_set_io_fd_own(recv_router_advertisement, true) >= 0); + + assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, + 2 * USEC_PER_SEC, 0, + NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); + + assert_se(sd_radv_start(ra) >= 0); + + assert_se(sd_event_loop(e) >= 0); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/libsystemd-network/test-ndisc-rs.c b/src/libsystemd-network/test-ndisc-rs.c new file mode 100644 index 0000000..d94cc1c --- /dev/null +++ b/src/libsystemd-network/test-ndisc-rs.c @@ -0,0 +1,339 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2014 Intel Corporation. All rights reserved. +***/ + +#include <netinet/icmp6.h> +#include <arpa/inet.h> +#include <unistd.h> + +#include "sd-ndisc.h" + +#include "alloc-util.h" +#include "fd-util.h" +#include "hexdecoct.h" +#include "icmp6-util-unix.h" +#include "socket-util.h" +#include "strv.h" +#include "ndisc-internal.h" +#include "tests.h" + +static struct ether_addr mac_addr = { + .ether_addr_octet = {'A', 'B', 'C', '1', '2', '3'} +}; + +static bool verbose = false; +static sd_ndisc *test_timeout_nd; + +static void router_dump(sd_ndisc_router *rt) { + struct in6_addr addr; + uint8_t hop_limit; + usec_t t, lifetime; + uint64_t flags; + uint32_t mtu; + unsigned preference; + int r; + + assert_se(rt); + + log_info("--"); + assert_se(sd_ndisc_router_get_address(rt, &addr) >= 0); + log_info("Sender: %s", IN6_ADDR_TO_STRING(&addr)); + + assert_se(sd_ndisc_router_get_timestamp(rt, CLOCK_REALTIME, &t) >= 0); + log_info("Timestamp: %s", FORMAT_TIMESTAMP(t)); + + assert_se(sd_ndisc_router_get_timestamp(rt, CLOCK_MONOTONIC, &t) >= 0); + log_info("Monotonic: %" PRIu64, t); + + if (sd_ndisc_router_get_hop_limit(rt, &hop_limit) < 0) + log_info("No hop limit set"); + else + log_info("Hop limit: %u", hop_limit); + + assert_se(sd_ndisc_router_get_flags(rt, &flags) >= 0); + log_info("Flags: <%s|%s>", + flags & ND_RA_FLAG_OTHER ? "OTHER" : "", + flags & ND_RA_FLAG_MANAGED ? "MANAGED" : ""); + + assert_se(sd_ndisc_router_get_preference(rt, &preference) >= 0); + log_info("Preference: %s", + preference == SD_NDISC_PREFERENCE_LOW ? "low" : + preference == SD_NDISC_PREFERENCE_HIGH ? "high" : "medium"); + + assert_se(sd_ndisc_router_get_lifetime(rt, &lifetime) >= 0); + assert_se(sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_REALTIME, &t) >= 0); + log_info("Lifetime: %s (%s)", FORMAT_TIMESPAN(lifetime, USEC_PER_SEC), FORMAT_TIMESTAMP(t)); + + if (sd_ndisc_router_get_mtu(rt, &mtu) < 0) + log_info("No MTU set"); + else + log_info("MTU: %" PRIu32, mtu); + + r = sd_ndisc_router_option_rewind(rt); + for (;;) { + uint8_t type; + + assert_se(r >= 0); + + if (r == 0) + break; + + assert_se(sd_ndisc_router_option_get_type(rt, &type) >= 0); + + log_info(">> Option %u", type); + + switch (type) { + + case SD_NDISC_OPTION_SOURCE_LL_ADDRESS: + case SD_NDISC_OPTION_TARGET_LL_ADDRESS: { + _cleanup_free_ char *c = NULL; + const void *p; + size_t n; + + assert_se(sd_ndisc_router_option_get_raw(rt, &p, &n) >= 0); + assert_se(n > 2); + assert_se(c = hexmem((uint8_t*) p + 2, n - 2)); + + log_info("Address: %s", c); + break; + } + + case SD_NDISC_OPTION_PREFIX_INFORMATION: { + unsigned prefix_len; + uint8_t pfl; + struct in6_addr a; + + assert_se(sd_ndisc_router_prefix_get_valid_lifetime(rt, &lifetime) >= 0); + assert_se(sd_ndisc_router_prefix_get_valid_lifetime_timestamp(rt, CLOCK_REALTIME, &t) >= 0); + log_info("Valid Lifetime: %s (%s)", FORMAT_TIMESPAN(lifetime, USEC_PER_SEC), FORMAT_TIMESTAMP(t)); + + assert_se(sd_ndisc_router_prefix_get_preferred_lifetime(rt, &lifetime) >= 0); + assert_se(sd_ndisc_router_prefix_get_preferred_lifetime_timestamp(rt, CLOCK_REALTIME, &t) >= 0); + log_info("Preferred Lifetime: %s (%s)", FORMAT_TIMESPAN(lifetime, USEC_PER_SEC), FORMAT_TIMESTAMP(t)); + + assert_se(sd_ndisc_router_prefix_get_flags(rt, &pfl) >= 0); + log_info("Flags: <%s|%s>", + pfl & ND_OPT_PI_FLAG_ONLINK ? "ONLINK" : "", + pfl & ND_OPT_PI_FLAG_AUTO ? "AUTO" : ""); + + assert_se(sd_ndisc_router_prefix_get_prefixlen(rt, &prefix_len) >= 0); + log_info("Prefix Length: %u", prefix_len); + + assert_se(sd_ndisc_router_prefix_get_address(rt, &a) >= 0); + log_info("Prefix: %s", IN6_ADDR_TO_STRING(&a)); + + break; + } + + case SD_NDISC_OPTION_RDNSS: { + const struct in6_addr *a; + int n, i; + + n = sd_ndisc_router_rdnss_get_addresses(rt, &a); + assert_se(n > 0); + + for (i = 0; i < n; i++) + log_info("DNS: %s", IN6_ADDR_TO_STRING(a + i)); + + assert_se(sd_ndisc_router_rdnss_get_lifetime(rt, &lifetime) >= 0); + assert_se(sd_ndisc_router_rdnss_get_lifetime_timestamp(rt, CLOCK_REALTIME, &t) >= 0); + log_info("Lifetime: %s (%s)", FORMAT_TIMESPAN(lifetime, USEC_PER_SEC), FORMAT_TIMESTAMP(t)); + break; + } + + case SD_NDISC_OPTION_DNSSL: { + _cleanup_strv_free_ char **l = NULL; + int n, i; + + n = sd_ndisc_router_dnssl_get_domains(rt, &l); + assert_se(n > 0); + + for (i = 0; i < n; i++) + log_info("Domain: %s", l[i]); + + assert_se(sd_ndisc_router_dnssl_get_lifetime(rt, &lifetime) >= 0); + assert_se(sd_ndisc_router_dnssl_get_lifetime_timestamp(rt, CLOCK_REALTIME, &t) >= 0); + log_info("Lifetime: %s (%s)", FORMAT_TIMESPAN(lifetime, USEC_PER_SEC), FORMAT_TIMESTAMP(t)); + break; + }} + + r = sd_ndisc_router_option_next(rt); + } +} + +static int send_ra(uint8_t flags) { + uint8_t advertisement[] = { + 0x86, 0x00, 0xde, 0x83, 0x40, 0xc0, 0x00, 0xb4, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x01, 0xf4, + 0x00, 0x00, 0x01, 0xb8, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x19, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, + 0x20, 0x01, 0x0d, 0xb8, 0xde, 0xad, 0xbe, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x1f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, + 0x03, 0x6c, 0x61, 0x62, 0x05, 0x69, 0x6e, 0x74, + 0x72, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x78, 0x2b, 0xcb, 0xb3, 0x6d, 0x53, + }; + + advertisement[5] = flags; + + assert_se(write(test_fd[1], advertisement, sizeof(advertisement)) == + sizeof(advertisement)); + + if (verbose) + printf(" sent RA with flag 0x%02x\n", flags); + + return 0; +} + +static void test_callback(sd_ndisc *nd, sd_ndisc_event_t event, sd_ndisc_router *rt, void *userdata) { + sd_event *e = userdata; + static unsigned idx = 0; + uint64_t flags_array[] = { + 0, + 0, + 0, + ND_RA_FLAG_OTHER, + ND_RA_FLAG_MANAGED + }; + uint64_t flags; + + assert_se(nd); + + if (event != SD_NDISC_EVENT_ROUTER) + return; + + router_dump(rt); + + assert_se(sd_ndisc_router_get_flags(rt, &flags) >= 0); + assert_se(flags == flags_array[idx]); + idx++; + + if (verbose) + printf(" got event 0x%02" PRIx64 "\n", flags); + + if (idx < ELEMENTSOF(flags_array)) { + send_ra(flags_array[idx]); + return; + } + + sd_event_exit(e, 0); +} + +TEST(rs) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_ndisc_unrefp) sd_ndisc *nd = NULL; + + send_ra_function = send_ra; + + assert_se(sd_event_new(&e) >= 0); + + assert_se(sd_ndisc_new(&nd) >= 0); + assert_se(nd); + + assert_se(sd_ndisc_attach_event(nd, e, 0) >= 0); + + assert_se(sd_ndisc_set_ifindex(nd, 42) >= 0); + assert_se(sd_ndisc_set_mac(nd, &mac_addr) >= 0); + assert_se(sd_ndisc_set_callback(nd, test_callback, e) >= 0); + + assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, + 30 * USEC_PER_SEC, 0, + NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); + + assert_se(sd_ndisc_stop(nd) >= 0); + assert_se(sd_ndisc_start(nd) >= 0); + assert_se(sd_ndisc_start(nd) >= 0); + assert_se(sd_ndisc_stop(nd) >= 0); + test_fd[1] = safe_close(test_fd[1]); + + assert_se(sd_ndisc_start(nd) >= 0); + + assert_se(sd_event_loop(e) >= 0); + + test_fd[1] = safe_close(test_fd[1]); +} + +static int test_timeout_value(uint8_t flags) { + static int count = 0; + static usec_t last = 0; + sd_ndisc *nd = test_timeout_nd; + usec_t min, max; + + assert_se(nd); + assert_se(nd->event); + + if (++count >= 20) + sd_event_exit(nd->event, 0); + + if (last == 0) { + /* initial RT = IRT + RAND*IRT */ + min = NDISC_ROUTER_SOLICITATION_INTERVAL - + NDISC_ROUTER_SOLICITATION_INTERVAL / 10; + max = NDISC_ROUTER_SOLICITATION_INTERVAL + + NDISC_ROUTER_SOLICITATION_INTERVAL / 10; + } else { + /* next RT = 2*RTprev + RAND*RTprev */ + min = 2 * last - last / 10; + max = 2 * last + last / 10; + } + + /* final RT > MRT */ + if (last * 2 > NDISC_MAX_ROUTER_SOLICITATION_INTERVAL) { + min = NDISC_MAX_ROUTER_SOLICITATION_INTERVAL - + NDISC_MAX_ROUTER_SOLICITATION_INTERVAL / 10; + max = NDISC_MAX_ROUTER_SOLICITATION_INTERVAL + + NDISC_MAX_ROUTER_SOLICITATION_INTERVAL / 10; + } + + log_info("backoff timeout interval %2d %s%s <= %s <= %s", + count, + last * 2 > NDISC_MAX_ROUTER_SOLICITATION_INTERVAL ? "(max) ": "", + FORMAT_TIMESPAN(min, USEC_PER_MSEC), + FORMAT_TIMESPAN(nd->retransmit_time, USEC_PER_MSEC), + FORMAT_TIMESPAN(max, USEC_PER_MSEC)); + + assert_se(min <= nd->retransmit_time); + assert_se(max >= nd->retransmit_time); + + last = nd->retransmit_time; + + assert_se(sd_event_source_set_time(nd->timeout_event_source, 0) >= 0); + + return 0; +} + +TEST(timeout) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_ndisc_unrefp) sd_ndisc *nd = NULL; + + send_ra_function = test_timeout_value; + + assert_se(sd_event_new(&e) >= 0); + + assert_se(sd_ndisc_new(&nd) >= 0); + assert_se(nd); + + test_timeout_nd = nd; + + assert_se(sd_ndisc_attach_event(nd, e, 0) >= 0); + + assert_se(sd_ndisc_set_ifindex(nd, 42) >= 0); + assert_se(sd_ndisc_set_mac(nd, &mac_addr) >= 0); + + assert_se(sd_event_add_time_relative(e, NULL, CLOCK_BOOTTIME, + 30 * USEC_PER_SEC, 0, + NULL, INT_TO_PTR(-ETIMEDOUT)) >= 0); + + assert_se(sd_ndisc_start(nd) >= 0); + + assert_se(sd_event_loop(e) >= 0); + + test_fd[1] = safe_close(test_fd[1]); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/libsystemd-network/test-sd-dhcp-lease.c b/src/libsystemd-network/test-sd-dhcp-lease.c new file mode 100644 index 0000000..910b622 --- /dev/null +++ b/src/libsystemd-network/test-sd-dhcp-lease.c @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> + +#include "dhcp-lease-internal.h" +#include "macro.h" +#include "string-util.h" +#include "strv.h" +#include "tests.h" + +/* According to RFC1035 section 4.1.4, a domain name in a message can be either: + * - a sequence of labels ending in a zero octet + * - a pointer + * - a sequence of labels ending with a pointer + */ +TEST(dhcp_lease_parse_search_domains_basic) { + int r; + _cleanup_strv_free_ char **domains = NULL; + static const uint8_t optionbuf[] = { + 0x03, 'F', 'O', 'O', 0x03, 'B', 'A', 'R', 0x00, + 0x04, 'A', 'B', 'C', 'D', 0x03, 'E', 'F', 'G', 0x00, + }; + + r = dhcp_lease_parse_search_domains(optionbuf, sizeof(optionbuf), &domains); + assert_se(r == 2); + assert_se(streq(domains[0], "FOO.BAR")); + assert_se(streq(domains[1], "ABCD.EFG")); +} + +TEST(dhcp_lease_parse_search_domains_ptr) { + int r; + _cleanup_strv_free_ char **domains = NULL; + static const uint8_t optionbuf[] = { + 0x03, 'F', 'O', 'O', 0x00, 0xC0, 0x00, + }; + + r = dhcp_lease_parse_search_domains(optionbuf, sizeof(optionbuf), &domains); + assert_se(r == 2); + assert_se(streq(domains[0], "FOO")); + assert_se(streq(domains[1], "FOO")); +} + +TEST(dhcp_lease_parse_search_domains_labels_and_ptr) { + int r; + _cleanup_strv_free_ char **domains = NULL; + static const uint8_t optionbuf[] = { + 0x03, 'F', 'O', 'O', 0x03, 'B', 'A', 'R', 0x00, + 0x03, 'A', 'B', 'C', 0xC0, 0x04, + }; + + r = dhcp_lease_parse_search_domains(optionbuf, sizeof(optionbuf), &domains); + assert_se(r == 2); + assert_se(streq(domains[0], "FOO.BAR")); + assert_se(streq(domains[1], "ABC.BAR")); +} + +/* Tests for exceptions. */ + +TEST(dhcp_lease_parse_search_domains_no_data) { + _cleanup_strv_free_ char **domains = NULL; + static const uint8_t optionbuf[3] = {0, 0, 0}; + + assert_se(dhcp_lease_parse_search_domains(NULL, 0, &domains) == -EBADMSG); + assert_se(dhcp_lease_parse_search_domains(optionbuf, 0, &domains) == -EBADMSG); +} + +TEST(dhcp_lease_parse_search_domains_loops) { + _cleanup_strv_free_ char **domains = NULL; + static const uint8_t optionbuf[] = { + 0x03, 'F', 'O', 'O', 0x00, 0x03, 'B', 'A', 'R', 0xC0, 0x06, + }; + + assert_se(dhcp_lease_parse_search_domains(optionbuf, sizeof(optionbuf), &domains) == -EBADMSG); +} + +TEST(dhcp_lease_parse_search_domains_wrong_len) { + _cleanup_strv_free_ char **domains = NULL; + static const uint8_t optionbuf[] = { + 0x03, 'F', 'O', 'O', 0x03, 'B', 'A', 'R', 0x00, + 0x04, 'A', 'B', 'C', 'D', 0x03, 'E', 'F', 'G', 0x00, + }; + + assert_se(dhcp_lease_parse_search_domains(optionbuf, sizeof(optionbuf) - 5, &domains) == -EBADMSG); +} + +DEFINE_TEST_MAIN(LOG_INFO); |