diff --git a/shared/n-dhcp4/.cherryci/ci-test b/shared/n-dhcp4/.cherryci/ci-test new file mode 100755 index 000000000..e1fc8b910 --- /dev/null +++ b/shared/n-dhcp4/.cherryci/ci-test @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +rm -Rf "./ci-build" +mkdir "./ci-build" +cd "./ci-build" + +${CHERRY_LIB_MESONSETUP} . "${CHERRY_LIB_SRCDIR}" +${CHERRY_LIB_NINJABUILD} +${CHERRY_LIB_MESONTEST} +# no valgrind tests, since setns(2) is not supported by it diff --git a/shared/n-dhcp4/.editorconfig b/shared/n-dhcp4/.editorconfig new file mode 100644 index 000000000..b10bb4f3f --- /dev/null +++ b/shared/n-dhcp4/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{c,h}] +indent_style = space +indent_size = 8 diff --git a/shared/n-dhcp4/.gitmodules b/shared/n-dhcp4/.gitmodules new file mode 100644 index 000000000..ffe679226 --- /dev/null +++ b/shared/n-dhcp4/.gitmodules @@ -0,0 +1,9 @@ +[submodule "subprojects/c-list"] + path = subprojects/c-list + url = https://github.com/c-util/c-list.git +[submodule "subprojects/c-siphash"] + path = subprojects/c-siphash + url = https://github.com/c-util/c-siphash.git +[submodule "subprojects/c-stdaux"] + path = subprojects/c-stdaux + url = https://github.com/c-util/c-stdaux.git diff --git a/shared/n-dhcp4/.travis.yml b/shared/n-dhcp4/.travis.yml new file mode 100644 index 000000000..99a7bb946 --- /dev/null +++ b/shared/n-dhcp4/.travis.yml @@ -0,0 +1,21 @@ +os: linux +dist: trusty +language: c + +services: + - docker + +before_install: + - curl -O -L "https://raw.githubusercontent.com/cherry-pick/cherry-images/v1/scripts/vmrun" + - curl -O -L "https://raw.githubusercontent.com/cherry-pick/cherry-ci/v1/scripts/cherryci" + - chmod +x "./vmrun" "./cherryci" + +jobs: + include: + - stage: test + script: + - ./vmrun -- ../src/cherryci -d ../src/.cherryci -s c-util -m + - script: + - ./vmrun -T armv7hl -- ../src/cherryci -d ../src/.cherryci -s c-util + - script: + - ./vmrun -T i686 -- ../src/cherryci -d ../src/.cherryci -s c-util diff --git a/shared/n-dhcp4/AUTHORS b/shared/n-dhcp4/AUTHORS new file mode 100644 index 000000000..b59660c5e --- /dev/null +++ b/shared/n-dhcp4/AUTHORS @@ -0,0 +1,37 @@ +LICENSE: + This project is dual-licensed under both the Apache License, Version + 2.0, and the GNU Lesser General Public License, Version 2.1+. + +AUTHORS-ASL: + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +AUTHORS-LGPL: + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; If not, see . + +COPYRIGHT: (ordered alphabetically) + Copyright (C) 2015-2019 Red Hat, Inc. + +AUTHORS: (ordered alphabetically) + David Rheinsberg + Tom Gundersen diff --git a/shared/n-dhcp4/NEWS.md b/shared/n-dhcp4/NEWS.md new file mode 100644 index 000000000..b41de08ab --- /dev/null +++ b/shared/n-dhcp4/NEWS.md @@ -0,0 +1,20 @@ +# n-dhcp4 - Dynamic Host Configuration Protocol for IPv4 + +## CHANGES WITH 1: + + * Initial release of n-dhcp4, an implementation of the IPv4 Dynamic + Host Configuration Protocol as defined in RFC-2132+. + + * This is a pre-release. The code is not yet ready for production. + Furthermore, the server implementation is incomplete and subject to + change. + The client implementation is considered complete, but the API is not + set in stone. We will have to adapt it according to the needs of the + users. Hence, this release does not provide any ABI stability + guarantees, yet. + + * The n-dhcp4 project is now dual-licensed: ASL-2.0 and LGPL-2.1+ + + Contributions from: David Rheinsberg, Tom Gundersen + + - Tübingen, 2019-05-10 diff --git a/shared/n-dhcp4/README.md b/shared/n-dhcp4/README.md new file mode 100644 index 000000000..298b49727 --- /dev/null +++ b/shared/n-dhcp4/README.md @@ -0,0 +1,53 @@ +n-dhcp4 +======= + +Dynamic Host Configuration Protocol for IPv4 + +The n-dhcp4 project implements the IPv4 Dynamic Host Configuration Protocol as +defined in RFC-2132+. + +### Project + + * **Website**: + * **Bug Tracker**: + * **Mailing-List**: + +### Requirements + +The requirements for this project are: + + * `Linux kernel >= 3.19` + * `libc` (e.g., `glibc >= 2.16`) + +At build-time, the following software is required: + + * `meson >= 0.41` + * `pkg-config >= 0.29` + +### Build + +The meson build-system is used for this project. Contact upstream +documentation for detailed help. In most situations the following +commands are sufficient to build and install from source: + +```sh +mkdir build +cd build +meson setup .. +ninja +meson test +ninja install +``` + +No custom configuration options are available. + +### Repository: + + - **web**: + - **https**: `https://github.com/nettools/n-dhcp4.git` + - **ssh**: `git@github.com:nettools/n-dhcp4.git` + +### License: + + - **Apache-2.0** OR **LGPL-2.1-or-later** + - See AUTHORS file for details. diff --git a/shared/n-dhcp4/meson.build b/shared/n-dhcp4/meson.build new file mode 100644 index 000000000..1dddfe4f0 --- /dev/null +++ b/shared/n-dhcp4/meson.build @@ -0,0 +1,23 @@ +project( + 'n-dhcp4', + 'c', + version: '1', + license: 'Apache', + default_options: [ + 'c_std=c11', + ], +) +project_description = 'Dynamic Host Configuration Protocol for IPv4' + +add_project_arguments('-D_GNU_SOURCE', language: 'c') +mod_pkgconfig = import('pkgconfig') + +sub_clist = subproject('c-list') +sub_csiphash = subproject('c-siphash') +sub_cstdaux = subproject('c-stdaux') + +dep_clist = sub_clist.get_variable('libclist_dep') +dep_csiphash = sub_csiphash.get_variable('libcsiphash_dep') +dep_cstdaux = sub_cstdaux.get_variable('libcstdaux_dep') + +subdir('src') diff --git a/shared/n-dhcp4/src/libndhcp4.sym b/shared/n-dhcp4/src/libndhcp4.sym new file mode 100644 index 000000000..b9ba800a3 --- /dev/null +++ b/shared/n-dhcp4/src/libndhcp4.sym @@ -0,0 +1,66 @@ +LIBNDHCP4_1 { +global: + n_dhcp4_client_config_new; + n_dhcp4_client_config_free; + n_dhcp4_client_config_set_ifindex; + n_dhcp4_client_config_set_transport; + n_dhcp4_client_config_set_request_broadcast; + n_dhcp4_client_config_set_mac; + n_dhcp4_client_config_set_broadcast_mac; + n_dhcp4_client_config_set_client_id; + + n_dhcp4_client_probe_config_new; + n_dhcp4_client_probe_config_free; + n_dhcp4_client_probe_config_set_inform_only; + n_dhcp4_client_probe_config_set_init_reboot; + n_dhcp4_client_probe_config_set_requested_ip; + n_dhcp4_client_probe_config_set_start_delay; + n_dhcp4_client_probe_config_request_option; + n_dhcp4_client_probe_config_append_option; + + n_dhcp4_client_new; + n_dhcp4_client_ref; + n_dhcp4_client_unref; + n_dhcp4_client_get_fd; + n_dhcp4_client_dispatch; + n_dhcp4_client_pop_event; + n_dhcp4_client_update_mtu; + n_dhcp4_client_probe; + + n_dhcp4_client_probe_free; + n_dhcp4_client_probe_get_userdata; + n_dhcp4_client_probe_set_userdata; + + n_dhcp4_client_lease_ref; + n_dhcp4_client_lease_unref; + n_dhcp4_client_lease_get_yiaddr; + n_dhcp4_client_lease_get_lifetime; + n_dhcp4_client_lease_query; + n_dhcp4_client_lease_select; + n_dhcp4_client_lease_accept; + n_dhcp4_client_lease_decline; + + n_dhcp4_server_config_new; + n_dhcp4_server_config_free; + n_dhcp4_server_config_set_ifindex; + + n_dhcp4_server_new; + n_dhcp4_server_ref; + n_dhcp4_server_unref; + n_dhcp4_server_get_fd; + n_dhcp4_server_dispatch; + n_dhcp4_server_pop_event; + n_dhcp4_server_add_ip; + + n_dhcp4_server_ip_free; + + n_dhcp4_server_lease_ref; + n_dhcp4_server_lease_unref; + n_dhcp4_server_lease_query; + n_dhcp4_server_lease_append; + n_dhcp4_server_lease_offer; + n_dhcp4_server_lease_ack; + n_dhcp4_server_lease_nack; +local: + *; +}; diff --git a/shared/n-dhcp4/src/meson.build b/shared/n-dhcp4/src/meson.build new file mode 100644 index 000000000..2ac8ac1cb --- /dev/null +++ b/shared/n-dhcp4/src/meson.build @@ -0,0 +1,91 @@ +# +# target: libndhcp4.so +# + +libndhcp4_symfile = join_paths(meson.current_source_dir(), 'libndhcp4.sym') + +libndhcp4_deps = [ + dep_clist, + dep_csiphash, + dep_cstdaux, +] + +libndhcp4_private = static_library( + 'ndhcp4-private', + [ + 'n-dhcp4-c-connection.c', + 'n-dhcp4-c-lease.c', + 'n-dhcp4-c-probe.c', + 'n-dhcp4-client.c', + 'n-dhcp4-incoming.c', + 'n-dhcp4-outgoing.c', + 'n-dhcp4-s-connection.c', + 'n-dhcp4-s-lease.c', + 'n-dhcp4-server.c', + 'n-dhcp4-socket.c', + 'util/link.c', + 'util/netns.c', + 'util/packet.c', + 'util/socket.c', + ], + c_args: [ + '-fvisibility=hidden', + '-fno-common' + ], + dependencies: libndhcp4_deps, + pic: true, +) + +libndhcp4_shared = shared_library( + 'ndhcp4', + objects: libndhcp4_private.extract_all_objects(), + dependencies: libndhcp4_deps, + install: not meson.is_subproject(), + soversion: 0, + link_depends: libndhcp4_symfile, + link_args: [ + '-Wl,--no-undefined', + '-Wl,--version-script=@0@'.format(libndhcp4_symfile) + ], +) + +libndhcp4_dep = declare_dependency( + include_directories: include_directories('.'), + link_with: libndhcp4_private, + dependencies: libndhcp4_deps, + version: meson.project_version(), +) + +if not meson.is_subproject() + install_headers('n-dhcp4.h') + + mod_pkgconfig.generate( + libraries: libndhcp4_shared, + version: meson.project_version(), + name: 'libndhcp4', + filebase: 'libndhcp4', + description: project_description, + ) +endif + +# +# target: test-* +# + +test_api = executable('test-api', ['test-api.c'], link_with: libndhcp4_shared) +test('API Symbol Visibility', test_api) + +test_connection = executable('test-connection', ['test-connection.c'], dependencies: libndhcp4_dep) +test('Connection Handling', test_connection) + +test_message = executable('test-message', ['test-message.c'], dependencies: libndhcp4_dep) +test('Message Handling', test_message) + +test_run_client = executable('test-run-client', ['test-run-client.c'], dependencies: libndhcp4_dep) +test('Client Runner', test_run_client, args: ['--test']) + +test_socket = executable('test-socket', ['test-socket.c'], dependencies: libndhcp4_dep) +test('Socket Handling', test_socket) + +test_util_packet = executable('test-util-packet', ['util/test-packet.c'], dependencies: libndhcp4_dep) +test('Packet Utility Library', test_util_packet) diff --git a/shared/n-dhcp4/src/n-dhcp4-c-connection.c b/shared/n-dhcp4/src/n-dhcp4-c-connection.c new file mode 100644 index 000000000..2d099b41f --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-c-connection.c @@ -0,0 +1,1147 @@ +/* + * DHCPv4 Client Connection + * + * XXX + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4-private.h" +#include "util/packet.h" + +/** + * n_dhcp4_c_connection_init() - initialize client connection + * @connection: connection to operate on + * @client_config: client configuration to use + * @probe_config: client probe configuration to use + * @fd_epoll: epoll context to attach to, or -1 + * + * This initializes a new client connection using the configuration given in + * @client_config and @probe_config. + * + * The client-configuration given as @client_config must survive the lifetime + * of @connection. It is pinned in the connection and used all over the place. + * The caller must guarantee that the configuration is not deallocated in the + * meantime. Same is true for @probe_config. + * + * The new connection automatically attaches to the epoll context given as + * @fd_epoll. The epoll FD is retained in the connection and the caller must + * guarantee that it lives as long as the connection. + * The caller is explicitly allowed to pass -1 as @fd_epoll, in which case the + * connection will initialize correctly, but will not be in a usable state. + * That is, any call to n_dhcp4_c_connection_listen() will fail, since it will + * be unable to attach to the epoll context. Such a connection can be used to + * get a detached object that behaves sound, but provides no runtime. + * + * Return: 0 on success, negative error code on failure. + */ +int n_dhcp4_c_connection_init(NDhcp4CConnection *connection, + NDhcp4ClientConfig *client_config, + NDhcp4ClientProbeConfig *probe_config, + int fd_epoll) { + *connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection); + connection->client_config = client_config; + connection->probe_config = probe_config; + connection->fd_epoll = fd_epoll; + + /* + * We explicitly allow initializing connections with an invalid + * epoll-fd. The resulting connection immediately transitions into the + * CLOSED state. This allows the caller to create dummy connections + * useful to provide asynchronous constructor-feedback in the API. + * + * The effect of this is as if you immediately call + * n_dhcp4_c_connection_close() on the new connection. However, by + * directly passing -1 in the constructor, you are guaranteed not even + * the constructor can ever mess with your epoll-set. + */ + if (connection->fd_epoll < 0) + connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED; + + return 0; +} + +/** + * n_dhcp4_c_connection_deinit() - deinitialize client connection + * @connection: connection to operate on + * + * This deinitializes a connection that was previously initialized via + * n_dhcp4_c_connection_init(). It will tear down all allocated state and + * release it. + * + * Once this function returns, @connection is re-initialized to + * N_DHCP4_C_CONNECTION_NULL. If this function is called on a deinitialized + * connection, it is a no-op. + */ +void n_dhcp4_c_connection_deinit(NDhcp4CConnection *connection) { + n_dhcp4_c_connection_close(connection); + *connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection); +} + +static void n_dhcp4_c_connection_outgoing_set_secs(NDhcp4Outgoing *message) { + uint32_t secs; + + /* + * This function sets the `secs` field for outgoing messages. It + * expects the base-time and start-time to be already set by the + * caller. + * For a given outgoing message, its `secs` field describes the time + * (in seconds) between the start of the transaction this message is + * part of and the start of the operational process (also called the + * base time here). + * + * The operational process in the DHCP sense describes the entire + * process of requesting a lease and acquiring it. That is, it starts + * with the caller's intent to request a lease, and it ends when we + * got granted a lease. The act of refreshing a lease is, in itself, a + * new operational process. The base-time describes the start-time + * recorded when such a process as initiated. + * + * A transaction in the DHCP sense describes a request+reply + * combination, in most cases. That is, the time a request is sent is + * the start-time of a transaction. In the ideal case, the start-time + * of the first transaction in an operational process matches the + * base-time. However, transactions are often delayed with a randomized + * offset to reduce traffic during network bursts. + * In some cases, however, transactions are composed out of multiple + * requests+reply combinations. This includes, for instance, the SELECT + * message following an OFFER. The specification clearly says that + * those must be considered a single transaction and thus share the + * transaction start-time. + * + * The `secs` field, thus, describes how long a client has been busy + * requesting a lease. DHCP servers and proxies do use it to prioritize + * clients. + * + * Note: Some DHCP relays reject a `secs` value of 0 (which might look + * like it is uninitialized). Hence, we always clamp the value to + * the range `[1, INF[`. + */ + + secs = message->userdata.base_time - message->userdata.start_time; + secs /= 1000ULL * 1000ULL * 1000ULL; /* nsecs to secs */ + secs = secs ?: 1; /* clamp to `[1, INF[` */ + + n_dhcp4_outgoing_set_secs(message, secs); +} + +int n_dhcp4_c_connection_listen(NDhcp4CConnection *connection) { + _c_cleanup_(c_closep) int fd_packet = -1; + int r; + + c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_INIT); + + r = n_dhcp4_c_socket_packet_new(&fd_packet, connection->client_config->ifindex); + if (r) + return r; + + r = epoll_ctl(connection->fd_epoll, + EPOLL_CTL_ADD, + fd_packet, + &(struct epoll_event){ + .events = EPOLLIN, + .data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO }, + }); + if (r < 0) + return -errno; + + connection->state = N_DHCP4_C_CONNECTION_STATE_PACKET; + connection->fd_packet = fd_packet; + fd_packet = -1; + return 0; +} + +int n_dhcp4_c_connection_connect(NDhcp4CConnection *connection, + const struct in_addr *client, + const struct in_addr *server) { + int r, fd_udp; + + c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET); + + r = n_dhcp4_c_socket_udp_new(&fd_udp, + connection->client_config->ifindex, + client, + server); + if (r) + return r; + + r = epoll_ctl(connection->fd_epoll, + EPOLL_CTL_ADD, + fd_udp, + &(struct epoll_event){ + .events = EPOLLIN, + .data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO }, + }); + if (r < 0) { + r = -errno; + goto exit_fd; + } + + r = packet_shutdown(connection->fd_packet); + if (r < 0) + goto exit_epoll; + + connection->state = N_DHCP4_C_CONNECTION_STATE_DRAINING; + connection->fd_udp = fd_udp; + connection->client_ip = client->s_addr; + connection->server_ip = server->s_addr; + fd_udp = -1; + return 0; + +exit_epoll: + epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, fd_udp, NULL); +exit_fd: + close(fd_udp); + return r; +} + +void n_dhcp4_c_connection_close(NDhcp4CConnection *connection) { + if (connection->fd_udp >= 0) { + epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_udp, NULL); + connection->fd_udp = c_close(connection->fd_udp); + } + + if (connection->fd_packet >= 0) { + epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL); + connection->fd_packet = c_close(connection->fd_packet); + } + + connection->fd_epoll = -1; + connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED; +} + +static int n_dhcp4_c_connection_verify_incoming(NDhcp4CConnection *connection, + NDhcp4Incoming *message, + uint8_t *typep) { + NDhcp4Header *header = n_dhcp4_incoming_get_header(message); + uint8_t type; + uint32_t request_xid; + uint8_t *id; + size_t n_id; + int r; + + r = n_dhcp4_incoming_query_message_type(message, &type); + if (r) { + if (r == N_DHCP4_E_UNSET) + return N_DHCP4_E_MALFORMED; + else + return r; + } + + switch (type) { + case N_DHCP4_MESSAGE_OFFER: + case N_DHCP4_MESSAGE_ACK: + case N_DHCP4_MESSAGE_NAK: + /* + * Only accept replies if there is a pending request, and it + * has a matching transaction id. + */ + if (!connection->request) + return N_DHCP4_E_UNEXPECTED; + + n_dhcp4_outgoing_get_xid(connection->request, &request_xid); + if (header->xid != request_xid) + return N_DHCP4_E_UNEXPECTED; + + break; + case N_DHCP4_MESSAGE_FORCERENEW: + /* + * Force renew messages are triggered by a server, and do not + * match a pending request. + */ + break; + default: + return N_DHCP4_E_UNEXPECTED; + } + + /* + * In case our transport makes use of the 'chaddr' field, make sure it + * matches exactly our address. + */ + switch (connection->client_config->transport) { + case N_DHCP4_TRANSPORT_ETHERNET: + c_assert(connection->client_config->n_mac == ETH_ALEN); + + if (header->hlen != ETH_ALEN) + return N_DHCP4_E_UNEXPECTED; + if (memcmp(header->chaddr, connection->client_config->mac, ETH_ALEN) != 0) + return N_DHCP4_E_UNEXPECTED; + + break; + case N_DHCP4_TRANSPORT_INFINIBAND: + if (header->hlen != 0) + return N_DHCP4_E_UNEXPECTED; + + break; + } + + /* + * If a server passes us back a client ID, it must be the one we + * provided. We ignore any packets that have mismatching client-ids. + */ + id = NULL; + n_id = 0; + r = n_dhcp4_incoming_query(message, N_DHCP4_OPTION_CLIENT_IDENTIFIER, &id, &n_id); + if (r) { + if (r != N_DHCP4_E_UNSET) + return r; + } else { + if (n_id != connection->client_config->n_client_id) + return N_DHCP4_E_UNEXPECTED; + if (memcmp(id, connection->client_config->client_id, n_id) != 0) + return N_DHCP4_E_UNEXPECTED; + } + + *typep = type; + return 0; +} + +void n_dhcp4_c_connection_get_timeout(NDhcp4CConnection *connection, + uint64_t *timeoutp) { + uint64_t timeout; + size_t n_send; + + if (!connection->request) { + *timeoutp = 0; + return; + } + + switch (connection->request->userdata.type) { + case N_DHCP4_C_MESSAGE_DISCOVER: + case N_DHCP4_C_MESSAGE_SELECT: + case N_DHCP4_C_MESSAGE_REBOOT: + case N_DHCP4_C_MESSAGE_INFORM: + /* + * Resend with an exponential backoff and a one second random + * slack, from a minimum of two seconds to a maximum of sixty + * four. + * + * Note that the RFC says to start at four rather than two + * seconds, and use [-1,1] slack, rather than [0,1]. + */ + n_send = connection->request->userdata.n_send; + if (n_send >= 6) + n_send = 6; + + timeout = connection->request->userdata.send_time + ((1ULL << n_send) * 1000000000ULL) + connection->request->userdata.send_jitter; + + break; + case N_DHCP4_C_MESSAGE_REBIND: + case N_DHCP4_C_MESSAGE_RENEW: + /* + * Resend every sixty seconds with a one second random slack. + * + * Note that the RFC says to do this at most once, but we do + * it until we are cancelled. + */ + timeout = connection->request->userdata.send_time + (60ULL * 1000000000ULL) + connection->request->userdata.send_jitter; + + break; + case N_DHCP4_C_MESSAGE_DECLINE: + case N_DHCP4_C_MESSAGE_RELEASE: + /* XXX make sure these message types are never pinned? */ + timeout = 0; + break; + default: + c_assert(0); + } + + *timeoutp = timeout; +} + +static int n_dhcp4_c_connection_packet_broadcast(NDhcp4CConnection *connection, + NDhcp4Outgoing *message) { + int r; + + c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET); + + r = n_dhcp4_c_socket_packet_send(connection->fd_packet, + connection->client_config->ifindex, + connection->client_config->broadcast_mac, + connection->client_config->n_broadcast_mac, + message); + if (r) + return r; + + return 0; +} + +static int n_dhcp4_c_connection_udp_broadcast(NDhcp4CConnection *connection, + NDhcp4Outgoing *message) { + int r; + + c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING || + connection->state == N_DHCP4_C_CONNECTION_STATE_UDP); + + r = n_dhcp4_c_socket_udp_broadcast(connection->fd_udp, message); + if (r) + return r; + + return 0; +} + +static int n_dhcp4_c_connection_udp_send(NDhcp4CConnection *connection, + NDhcp4Outgoing *message) { + int r; + + c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING || + connection->state == N_DHCP4_C_CONNECTION_STATE_UDP); + + r = n_dhcp4_c_socket_udp_send(connection->fd_udp, message); + if (r) + return r; + + return 0; +} + +static void n_dhcp4_c_connection_init_header(NDhcp4CConnection *connection, + NDhcp4Header *header) { + bool broadcast = connection->client_config->request_broadcast; + + header->op = N_DHCP4_OP_BOOTREQUEST; + + switch (connection->client_config->transport) { + case N_DHCP4_TRANSPORT_ETHERNET: + c_assert(connection->client_config->n_mac == ETH_ALEN); + + header->htype = ARPHRD_ETHER; + header->hlen = ETH_ALEN; + memcpy(header->chaddr, connection->client_config->mac, ETH_ALEN); + break; + case N_DHCP4_TRANSPORT_INFINIBAND: + header->htype = ARPHRD_INFINIBAND; + header->hlen = 0; + + /* infiniband mandates to request broadcasts */ + broadcast = true; + break; + default: + abort(); + break; + } + + if (connection->client_ip != INADDR_ANY) { + header->ciaddr = connection->client_ip; + } else { + /* + * When the IP stack has not been configured, we may + * not be able to receive unicast packets, depending + * on the hardware. If that is the case we must request + * replies from the server to be broadcast. + * + * Once the IP stack has been configured, receiving + * unicast packets is never a problem, so the broadcast + * flag should not be set. + */ + if (broadcast) + header->flags |= N_DHCP4_MESSAGE_FLAG_BROADCAST; + } +} + +static int n_dhcp4_c_connection_new_message(NDhcp4CConnection *connection, + NDhcp4Outgoing **messagep, + uint8_t type) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + NDhcp4Header *header; + uint8_t message_type; + bool via_packet_socket = false; + int r; + + switch (type) { + case N_DHCP4_C_MESSAGE_DISCOVER: + message_type = N_DHCP4_MESSAGE_DISCOVER; + via_packet_socket = true; + break; + case N_DHCP4_C_MESSAGE_INFORM: + message_type = N_DHCP4_MESSAGE_INFORM; + break; + case N_DHCP4_C_MESSAGE_SELECT: + message_type = N_DHCP4_MESSAGE_REQUEST; + via_packet_socket = true; + break; + case N_DHCP4_C_MESSAGE_RENEW: + message_type = N_DHCP4_MESSAGE_REQUEST; + break; + case N_DHCP4_C_MESSAGE_REBIND: + message_type = N_DHCP4_MESSAGE_REQUEST; + break; + case N_DHCP4_C_MESSAGE_REBOOT: + message_type = N_DHCP4_MESSAGE_REQUEST; + via_packet_socket = true; + break; + case N_DHCP4_C_MESSAGE_RELEASE: + message_type = N_DHCP4_MESSAGE_RELEASE; + break; + case N_DHCP4_C_MESSAGE_DECLINE: + message_type = N_DHCP4_MESSAGE_DECLINE; + via_packet_socket = true; + break; + default: + abort(); + return -ENOTRECOVERABLE; + } + + /* + * We explicitly pass 0 as maximum message size, which makes + * NDhcp4Outgoing use the mandated default value from the spec (see its + * implementation). While the transport and like layers might support + * bigger MTUs (and we very likely know about them through + * n_dhcp4_client_update_mtu()), we cannot assume the target DHCP + * server supports parsing packets bigger than the minimum (and it is + * allowed to refuse bigger IP packets, even if the network supports + * transmission of them). + * + * We could theoretically increase this for packets other than the + * initial discovery. However, clients are unlikely to ever send large + * packets, so we just keep the same default for all outgoing packets. + */ + r = n_dhcp4_outgoing_new(&message, 0, N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME); + if (r) + return r; + + header = n_dhcp4_outgoing_get_header(message); + n_dhcp4_c_connection_init_header(connection, header); + + message->userdata.type = type; + + /* + * Note that some implementations expect the MESSAGE_TYPE option to be + * the first option, and possibly even hard-code access to it. Hence, + * we really should make sure to pass it first as well. + */ + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MESSAGE_TYPE, &message_type, sizeof(message_type)); + if (r) + return r; + + r = n_dhcp4_outgoing_append(message, + N_DHCP4_OPTION_CLIENT_IDENTIFIER, + connection->client_config->client_id, + connection->client_config->n_client_id); + if (r) + return r; + + switch (message_type) { + case N_DHCP4_MESSAGE_DISCOVER: + case N_DHCP4_MESSAGE_REQUEST: + case N_DHCP4_MESSAGE_INFORM: { + uint16_t mtu; + + if (connection->probe_config->n_request_parameters > 0) { + r = n_dhcp4_outgoing_append(message, + N_DHCP4_OPTION_PARAMETER_REQUEST_LIST, + connection->probe_config->request_parameters, + connection->probe_config->n_request_parameters); + if (r) + return r; + } + + if (via_packet_socket) { + /* + * In case of packet sockets, we do not support + * fragmentation. Hence, our maximum message size + * equals the transport MTU. In case no mtu is given, + * we use the minimum size mandated by the IP spec. If + * we omit the field, some implementations will + * interpret this to mean any packet size is supported, + * which we rather not want as default behavior (we can + * always support suppressing this field, if that is + * what the caller wants). + */ + mtu = htons(connection->mtu ?: N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE); + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu)); + if (r) + return r; + } else { + /* + * Once we use UDP sockets, we support fragmentation + * through the kernel IP stack. This means, the biggest + * message we can receive is the maximum UDP size plus + * the possible IP header. This would sum up to + * 2^16-1 + 20 (or even 2^16-1 + 60 if pedantic) and + * thus exceed the option field. Hence, we simply set + * the option to the maximum possible value. + */ + mtu = htons(UINT16_MAX); + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu)); + if (r) + return r; + } + + break; + } + default: + break; + } + + *messagep = message; + message = NULL; + return 0; +} + +/* + * RFC2131 3.1 + * + * The client broadcasts a DHCPDISCOVER message on its local physical + * subnet. The DHCPDISCOVER message MAY include options that suggest + * values for the network address and lease duration. BOOTP relay + * agents may pass the message on to DHCP servers not on the same + * physical subnet. + * + * RFC2131 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. + * + * [...] + * + * In addition, 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. Other options + * representing "hints" at configuration parameters are allowed in a + * DHCPDISCOVER or DHCPREQUEST message. + * + * RFC2131 4.4.1 + * + * The client generates and records a random transaction identifier and + * inserts that identifier into the 'xid' field. The client records its + * own local time for later use in computing the lease expiration. The + * client then broadcasts the DHCPDISCOVER on the local hardware + * broadcast address to the 0xffffffff IP broadcast address and 'DHCP + * server' UDP port. + * + * If the 'xid' of an arriving DHCPOFFER message does not match the + * 'xid' of the most recent DHCPDISCOVER message, the DHCPOFFER message + * must be silently discarded. Any arriving DHCPACK messages must be + * silently discarded. + */ +int n_dhcp4_c_connection_discover_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + int r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DISCOVER); + if (r) + return r; + + *requestp = message; + message = NULL; + return 0; +} + +/* + * + * RFC2131 4.1.1 + * + * The DHCPREQUEST message contains the same 'xid' as the DHCPOFFER + * message. + * + * RFC2131 4.3.2 + * + * 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. + */ +int n_dhcp4_c_connection_select_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp, + NDhcp4Incoming *offer) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + struct in_addr client; + struct in_addr server; + uint32_t xid; + int r; + + n_dhcp4_incoming_get_yiaddr(offer, &client); + + r = n_dhcp4_incoming_query_server_identifier(offer, &server); + if (r) + return r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_SELECT); + if (r) + return r; + + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client)); + if (r) + return r; + + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server)); + if (r) + return r; + + /* + * SELECT continues the transaction started by DISCOVER, and as such + * keeps the same start time. We also have to preserve the base time + * of the selected lease as well as the transaction ID. + */ + message->userdata.start_time = offer->userdata.start_time; + message->userdata.base_time = offer->userdata.base_time; + n_dhcp4_incoming_get_xid(offer, &xid); + n_dhcp4_outgoing_set_xid(message, xid); + + *requestp = message; + message = NULL; + return 0; +} + +/* + * RFC2131 4.3.2 + * + * '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. The client is seeking to + * verify a previously allocated, cached configuration. Server SHOULD + * send a DHCPNAK message to the client if the 'requested IP address' + * is incorrect, or is on the wrong network. + */ +int n_dhcp4_c_connection_reboot_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp, + const struct in_addr *client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + int r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBOOT); + if (r) + return r; + + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, client, sizeof(*client)); + if (r) + return r; + + *requestp = message; + message = NULL; + return 0; +} + +/* + * RFC2131 4.3.2 + * + * '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. In this situation, the client is completely + * configured, and is trying to extend its lease. This message will + * be unicast, so no relay agents will be involved in its + * transmission. Because 'giaddr' is therefore not filled in, the + * DHCP server will trust the value in 'ciaddr', and use it when + * replying to the client. + * + * A client MAY choose to renew or extend its lease prior to T1. The + * server may choose not to extend the lease (as a policy decision by + * the network administrator), but should return a DHCPACK message + * regardless. + * + * RFC2131 4.4.5 + * + * At time T1 the client moves to RENEWING state and sends (via unicast) + * a DHCPREQUEST message to the server to extend its lease. The client + * sets the 'ciaddr' field in the DHCPREQUEST to its current network + * address. The client records the local time at which the DHCPREQUEST + * message is sent for computation of the lease expiration time. The + * client MUST NOT include a 'server identifier' in the DHCPREQUEST + * message. + */ +int n_dhcp4_c_connection_renew_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + int r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RENEW); + if (r) + return r; + + *requestp = message; + message = NULL; + return 0; +} + +/* + * RFC2131 4.3.2 + * + * '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. In this situation, the client is completely + * configured, and is trying to extend its lease. This message MUST + * be broadcast to the 0xffffffff IP broadcast address. The DHCP + * server SHOULD check 'ciaddr' for correctness before replying to + * the DHCPREQUEST. + * + * RFC2131 4.4.5 + * + * If no DHCPACK arrives before time T2, the client moves to REBINDING + * state and sends (via broadcast) a DHCPREQUEST message to extend its + * lease. The client sets the 'ciaddr' field in the DHCPREQUEST to its + * current network address. The client MUST NOT include a 'server + * identifier' in the DHCPREQUEST message. + */ +int n_dhcp4_c_connection_rebind_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + int r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBIND); + if (r) + return r; + + *requestp = message; + message = NULL; + return 0; +} + +/* + * RFC2131 3.2 + * + * If the client detects that the IP address in the DHCPACK message + * is already in use, the client MUST send a DHCPDECLINE message to the + * server and restarts the configuration process by requesting a + * new network address. + * + * RFC2131 4.4.4 + * + * Because the client is declining the use of the IP address supplied by + * the server, the client broadcasts DHCPDECLINE messages. + */ +int n_dhcp4_c_connection_decline_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp, + NDhcp4Incoming *ack, + const char *error) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + struct in_addr client; + struct in_addr server; + int r; + + n_dhcp4_incoming_get_yiaddr(ack, &client); + + r = n_dhcp4_incoming_query_server_identifier(ack, &server); + if (r) + return r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DECLINE); + if (r) + return r; + + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client)); + if (r) + return r; + + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server)); + if (r) + return r; + + if (error) { + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1); + if (r) + return r; + } + + *requestp = message; + message = NULL; + return 0; +} + +/* + * RFC2131 3.4 + * + * If a client has obtained a network address through some other means + * (e.g., manual configuration), it may use a DHCPINFORM request message + * to obtain other local configuration parameters. + * + * RFC2131 4.4 + * + * The DHCPINFORM message is not shown in figure 5. A client simply + * sends the DHCPINFORM and waits for DHCPACK messages. Once the client + * has selected its parameters, it has completed the configuration + * process. + * + * RFC2131 4.4.3 + * + * The client sends a DHCPINFORM message. The client may request + * specific configuration parameters by including the 'parameter request + * list' option. The client generates and records a random transaction + * identifier and inserts that identifier into the 'xid' field. The + * client places its own network address in the 'ciaddr' field. The + * client SHOULD NOT request lease time parameters. + * + * The client then unicasts the DHCPINFORM to the DHCP server if it + * knows the server's address, otherwise it broadcasts the message to + * the limited (all 1s) broadcast address. DHCPINFORM messages MUST be + * directed to the 'DHCP server' UDP port. + */ +int n_dhcp4_c_connection_inform_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + int r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_INFORM); + if (r) + return r; + + *requestp = message; + message = NULL; + return 0; +} + +/* + * RFC2131 3.1 + * + * The client may choose to relinquish its lease on a network address + * by sending a DHCPRELEASE message to the server. The client + * identifies the lease to be released with its 'client identifier', + * or 'chaddr' and network address in the DHCPRELEASE message. If the + * client used a 'client identifier' when it obtained the lease, it + * MUST use the same 'client identifier' in the DHCPRELEASE message. + * + * RFC2131 3.2 + * + * The client may choose to relinquish its lease on a network + * address by sending a DHCPRELEASE message to the server. The + * client identifies the lease to be released with its + * 'client identifier', or 'chaddr' and network address in the + * DHCPRELEASE message. + * + * Note that in this case, where the client retains its network + * address locally, the client will not normally relinquish its + * lease during a graceful shutdown. Only in the case where the + * client explicitly needs to relinquish its lease, e.g., the client + * is about to be moved to a different subnet, will the client send + * a DHCPRELEASE message. + * + * RFC2131 4.4.4 + * + * The client unicasts DHCPRELEASE messages to the server. + * + * RFC2131 4.4.6 + * + * If the client no longer requires use of its assigned network address + * (e.g., the client is gracefully shut down), the client sends a + * DHCPRELEASE message to the server. Note that the correct operation + * of DHCP does not depend on the transmission of DHCPRELEASE messages. + */ +int n_dhcp4_c_connection_release_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **requestp, + const char *error) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + int r; + + r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RELEASE); + if (r) + return r; + + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &connection->server_ip, sizeof(connection->server_ip)); + if (r) + return r; + + if (error) { + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1); + if (r) + return r; + } + + *requestp = message; + message = NULL; + return 0; +} + +static int n_dhcp4_c_connection_send_request(NDhcp4CConnection *connection, + NDhcp4Outgoing *request, + uint64_t timestamp) { + int r; + + /* + * Increment the base time and reset the xid field, + * where applicable. We never alter the header on + * resends of SELECT, as it must always match the + * OFFER message they are in reply to. + */ + switch (request->userdata.type) { + case N_DHCP4_C_MESSAGE_DISCOVER: + case N_DHCP4_C_MESSAGE_INFORM: + case N_DHCP4_C_MESSAGE_REBOOT: + case N_DHCP4_C_MESSAGE_REBIND: + case N_DHCP4_C_MESSAGE_RENEW: + request->userdata.base_time = timestamp; + n_dhcp4_outgoing_set_xid(request, n_dhcp4_client_probe_config_get_random(connection->probe_config)); + + break; + case N_DHCP4_C_MESSAGE_SELECT: + case N_DHCP4_C_MESSAGE_DECLINE: + case N_DHCP4_C_MESSAGE_RELEASE: + break; + default: + c_assert(0); + } + + request->userdata.send_time = timestamp; + request->userdata.send_jitter = (n_dhcp4_client_probe_config_get_random(connection->probe_config) % 1000000000ULL); + n_dhcp4_c_connection_outgoing_set_secs(request); + + switch (request->userdata.type) { + case N_DHCP4_C_MESSAGE_DISCOVER: + case N_DHCP4_C_MESSAGE_SELECT: + case N_DHCP4_C_MESSAGE_REBOOT: + case N_DHCP4_C_MESSAGE_DECLINE: + r = n_dhcp4_c_connection_packet_broadcast(connection, request); + if (r) + return r; + break; + case N_DHCP4_C_MESSAGE_INFORM: + case N_DHCP4_C_MESSAGE_REBIND: + r = n_dhcp4_c_connection_udp_broadcast(connection, request); + if (r) + return r; + + break; + case N_DHCP4_C_MESSAGE_RENEW: + case N_DHCP4_C_MESSAGE_RELEASE: + r = n_dhcp4_c_connection_udp_send(connection, request); + if (r) + return r; + + break; + default: + c_assert(0); + } + + ++request->userdata.n_send; + return 0; +} + +int n_dhcp4_c_connection_start_request(NDhcp4CConnection *connection, + NDhcp4Outgoing *request, + uint64_t timestamp) { + int r; + + /* + * This function starts a request, but in the case of SELECT it + * continues a previous transaction, so we do not want to reset + * the start time. Only set the start time if it was not already + * set. + */ + if (request->userdata.start_time == 0) + request->userdata.start_time = timestamp; + + n_dhcp4_outgoing_free(connection->request); + connection->request = request; + + r = n_dhcp4_c_connection_send_request(connection, request, timestamp); + if (r) + return r; + + return 0; +} + +int n_dhcp4_c_connection_dispatch_timer(NDhcp4CConnection *connection, + uint64_t timestamp) { + uint64_t timeout; + int r; + + if (!connection->request) + return 0; + + n_dhcp4_c_connection_get_timeout(connection, &timeout); + + if (timeout > timestamp) + return 0; + + r = n_dhcp4_c_connection_send_request(connection, connection->request, timestamp); + if (r) + return r; + + return 0; +} + +int n_dhcp4_c_connection_dispatch_io(NDhcp4CConnection *connection, + NDhcp4Incoming **messagep) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + uint8_t type; + int r; + + switch (connection->state) { + case N_DHCP4_C_CONNECTION_STATE_PACKET: + r = n_dhcp4_c_socket_packet_recv(connection->fd_packet, + connection->scratch_buffer, + sizeof(connection->scratch_buffer), + &message); + if (r) + return r; + + break; + case N_DHCP4_C_CONNECTION_STATE_DRAINING: + r = n_dhcp4_c_socket_packet_recv(connection->fd_packet, + connection->scratch_buffer, + sizeof(connection->scratch_buffer), + &message); + if (!r) + break; + else if (r != N_DHCP4_E_AGAIN) + return r; + + /* + * The UDP socket is open and the packet socket has been shut down + * and drained, clean up the packet socket and fall through to + * dispatching the UDP socket. + */ + r = epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL); + c_assert(!r); + connection->fd_packet = c_close(connection->fd_packet); + connection->state = N_DHCP4_C_CONNECTION_STATE_UDP; + + /* fall-through */ + case N_DHCP4_C_CONNECTION_STATE_UDP: + r = n_dhcp4_c_socket_udp_recv(connection->fd_udp, + connection->scratch_buffer, + sizeof(connection->scratch_buffer), + &message); + if (r) + return r; + + break; + default: + abort(); + return -ENOTRECOVERABLE; + } + + r = n_dhcp4_c_connection_verify_incoming(connection, message, &type); + if (r) + return r; + + switch (type) { + case N_DHCP4_MESSAGE_OFFER: + case N_DHCP4_MESSAGE_ACK: + case N_DHCP4_MESSAGE_NAK: + /* + * Remember the start time of the transaction, and the base + * time of any relative timestamps from the pending request. + * Thes same times applies to the response, and sholud be + * copied over. + */ + message->userdata.start_time = connection->request->userdata.start_time; + message->userdata.base_time = connection->request->userdata.base_time; + + if (type != N_DHCP4_MESSAGE_OFFER) { + /* + * We only allow one reply to ACK or NAK, but for OFFER we must + * accept several, so we do not free the pinned request. + */ + connection->request = n_dhcp4_outgoing_free(connection->request); + } + + break; + default: + break; + } + + *messagep = message; + message = NULL; + return 0; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-c-lease.c b/shared/n-dhcp4/src/n-dhcp4-c-lease.c new file mode 100644 index 000000000..db1ffbae8 --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-c-lease.c @@ -0,0 +1,356 @@ +/* + * DHCP4 Client Leases + * + * This implements the public API wrapping DHCP4 client leases. A lease object + * conists of the information given to us from the server, together with the + * timestamp recording the start of the validity of the lease. + * + * A probe may yield many OFFERS, each of which contains a lease object. One of + * these offers may be SELECTED, which implicitly rejects all the others. + * The server may then ACK or NAK the lease which tells us whether or not we + * are permitted to start using it. Once an ACK has been received, we can + * configure the address, and only then can we SELECT the lease. If we + * determine that the offered lease was not appropriate after all we + * may DECLINE it instead. + */ + +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" + +/* + * Compute the absolute timeouts from an incoming message. A message contains relative timeouts and the userdata + * of the incoming message is set to the offset we must apply to get the absolute values. + * + * The special value UINT64_MAX is returned to indicate no or infinite timeouts. In case the given timeouts + * are invalid relative to each other, we recompute T1 and/or T2 to take their default values. Later timeouts + * take predecende above earlier ones (T1 is adjusted if it conflicts with T2, etc). + */ +static int n_dhcp4_incoming_get_timeouts(NDhcp4Incoming *message, uint64_t *t1p, uint64_t *t2p, uint64_t *lifetimep) { + uint64_t lifetime, t2, t1; + uint32_t u32; + int r; + + r = n_dhcp4_incoming_query_lifetime(message, &u32); + if (r == N_DHCP4_E_UNSET) { + lifetime = UINT64_MAX; + } else if (r) { + return r; + } else if (u32 == UINT32_MAX) { + lifetime = UINT64_MAX; + } else { + if (u32 == UINT32_MAX) + lifetime = UINT64_MAX; + else + lifetime = u32 * (1000000000ULL); + } + + r = n_dhcp4_incoming_query_t2(message, &u32); + if (r == N_DHCP4_E_UNSET) { + if (lifetime == UINT64_MAX) + t2 = UINT64_MAX; + else + t2 = (lifetime * 7) / 8; + } else if (r) { + return r; + } else { + if (u32 == UINT32_MAX) + t2 = UINT64_MAX; + else + t2 = u32 * (1000000000ULL); + + if (t2 > lifetime) + t2 = (lifetime * 7) / 8; + } + + r = n_dhcp4_incoming_query_t1(message, &u32); + if (r == N_DHCP4_E_UNSET) { + if (t2 == UINT64_MAX) + t1 = UINT64_MAX; + else + t1 = (t2 * 4) / 7; + } else if (r) { + return r; + } else { + if (u32 == UINT32_MAX) + t1 = UINT64_MAX; + else + t1 = u32 * (1000000000ULL); + + if (t1 > t2) + t1 = (t2 * 4) / 7; + } + + if (lifetime != UINT64_MAX) + lifetime += message->userdata.base_time; + if (t2 != UINT64_MAX) + t2 += message->userdata.base_time; + if (t1 != UINT64_MAX) + t1 += message->userdata.base_time; + + *lifetimep = lifetime; + *t2p = t2; + *t1p = t1; + return 0; +} + +/** + * n_dhcp4_client_lease_new() - allocate new client lease object + * @leasep: output argumnet for new client lease object + * @message: incoming message representing the lease + * + * This creates a new client lease object. Client lease objects are simple + * wrappers around an incoming message representing a lease. + * + * Return: 0 on success, negative error code on failure. + */ +int n_dhcp4_client_lease_new(NDhcp4ClientLease **leasep, NDhcp4Incoming *message) { + _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL; + int r; + + c_assert(leasep); + + lease = malloc(sizeof(*lease)); + if (!lease) + return -ENOMEM; + + *lease = (NDhcp4ClientLease)N_DHCP4_CLIENT_LEASE_NULL(*lease); + + r = n_dhcp4_incoming_get_timeouts(message, &lease->t1, &lease->t2, &lease->lifetime); + if (r) + return r; + + lease->message = message; + *leasep = lease; + lease = NULL; + return 0; +} + +static void n_dhcp4_client_lease_free(NDhcp4ClientLease *lease) { + n_dhcp4_client_lease_unlink(lease); + n_dhcp4_incoming_free(lease->message); + free(lease); +} + +/** + * n_dhcp4_client_lease_ref() - reference client lease + * @lease: the client lease object to reference + * + * Take a new reference to a client lease. + * + * Return: the lease. + */ +_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_ref(NDhcp4ClientLease *lease) { + if (lease) + ++lease->n_refs; + return lease; +} + +/** + * n_dhcp4_client_lease_unref() - dereference client lease + * @lease: the client lease object to dereference + * + * Relase a reference to a client lease. + * + * Return: NULL. + */ +_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_unref(NDhcp4ClientLease *lease) { + if (lease && !--lease->n_refs) + n_dhcp4_client_lease_free(lease); + return NULL; +} + +/** + * n_dhcp4_client_lease_link() - link lease into probe + * @lease: the lease to operate on + * @probe: the probe to link the lease into + * + * Associate a lease with a probe. The lease may not already be linked. + */ +void n_dhcp4_client_lease_link(NDhcp4ClientLease *lease, NDhcp4ClientProbe *probe) { + c_assert(!lease->probe); + c_assert(!c_list_is_linked(&lease->probe_link)); + + lease->probe = probe; + c_list_link_tail(&probe->lease_list, &lease->probe_link); +} + +/** + * n_dhcp4_client_lease_unlink() - unlinke lease from its probe + * @lease: the lease to operate on + * + * Dissassociate a lease from a probe if it is associated with one. Otherwise, + * this is a noop. + */ +void n_dhcp4_client_lease_unlink(NDhcp4ClientLease *lease) { + lease->probe = NULL; + c_list_unlink(&lease->probe_link); +} + +/** + * n_dhcp4_client_lease_get_yiaddr() - get the IP address + * @lease: the lease to operate on + * @yiaddr: return argument for the IP address + * + * Gets the IP address cotained in the lease. Or INADDR_ANY if the lease + * does not contain an IP address. + */ +_c_public_ void n_dhcp4_client_lease_get_yiaddr(NDhcp4ClientLease *lease, struct in_addr *yiaddr) { + NDhcp4Header *header = n_dhcp4_incoming_get_header(lease->message); + + yiaddr->s_addr = header->yiaddr; +} + +/** + * n_dhcp4_client_lease_get_lifetime() - get the lifetime + * @lease: the lease to operate on + * @ns_lifetimep: return argument for the lifetime in nano seconds + * + * Gets the end of the lease's lifetime in nanoseconds according to CLOCK_BOOTTIME, + * or (uint64_t)-1 for permanent leases. + */ +_c_public_ void n_dhcp4_client_lease_get_lifetime(NDhcp4ClientLease *lease, uint64_t *ns_lifetimep) { + *ns_lifetimep = lease->lifetime; +} + +/** + * n_dhcp4_client_lease_query() - query the lease for an option + * @lease: the lease to operate on + * @option: the DHCP4 option code + * @datap: return argument of the data pointer + * @n_datap: return argument of data length in bytes + * + * Query the lease for a given option. Options internal to the DHCP protocol cannot + * be queried, and only options that were explicitly requested can be queried. + * + * Return: 0 on success, + * N_DCHP4_E_INTERNAL if an invalid option is queried, + * N_DHCP4_E_UNSET if the lease did not contain the option, or + * a negative error code on failure. + */ +_c_public_ int n_dhcp4_client_lease_query(NDhcp4ClientLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap) { + switch (option) { + case N_DHCP4_OPTION_PAD: + case N_DHCP4_OPTION_REQUESTED_IP_ADDRESS: + case N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME: + case N_DHCP4_OPTION_OVERLOAD: + case N_DHCP4_OPTION_MESSAGE_TYPE: + case N_DHCP4_OPTION_SERVER_IDENTIFIER: + case N_DHCP4_OPTION_PARAMETER_REQUEST_LIST: + case N_DHCP4_OPTION_ERROR_MESSAGE: + case N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE: + case N_DHCP4_OPTION_RENEWAL_T1_TIME: + case N_DHCP4_OPTION_REBINDING_T2_TIME: + case N_DHCP4_OPTION_END: + return N_DHCP4_E_INTERNAL; + } + + /* XXX: refuse to return options that were not requested */ + + return n_dhcp4_incoming_query(lease->message, option, datap, n_datap); +} + +/** + * n_dhcp4_client_lease_select() - select an offered lease + * @lease: lease to operate on + * + * Select a lease. This must be a lease that was offered, once + * one of the leases that were offered in response to a probe was + * selected none of the others can be. + * + * Return: 0 on success, or a negative error code on failure. + */ +_c_public_ int n_dhcp4_client_lease_select(NDhcp4ClientLease *lease) { + NDhcp4ClientLease *l, *t_l; + NDhcp4ClientProbe *probe; + int r; + + /* XXX error handling, this must be an OFFER */ + + if (!lease->probe) + return -ENOTRECOVERABLE; + if (lease->probe->current_lease) + return -ENOTRECOVERABLE; + + r = n_dhcp4_client_probe_transition_select(lease->probe, lease->message, n_dhcp4_gettime(CLOCK_BOOTTIME)); + if (r) + return r; + + /* + * Only one of the offered leases can be selected, so flush the list. + * All offered lease, including this one are now dead. + */ + probe = lease->probe; + c_list_for_each_entry_safe(l, t_l, &probe->lease_list, probe_link) + n_dhcp4_client_lease_unlink(l); + + return 0; +} + +/** + * n_dhcp4_client_lease_accept() - accept an ack'ed lease + * @lease: lease to operate on + * + * Accept a lease. This must be a lease that was ack'ed by the + * server. + * + * The offered IP address must be fully configured before the lease + * can be accepted. + * + * Return: 0 on success, or a negative error code on failure. + */ +_c_public_ int n_dhcp4_client_lease_accept(NDhcp4ClientLease *lease) { + int r; + + /* XXX error handling, this must be an ACK */ + + if (!lease->probe) + return -ENOTRECOVERABLE; + if (lease->probe->current_lease != lease) + return -ENOTRECOVERABLE; + + r = n_dhcp4_client_probe_transition_accept(lease->probe, lease->message); + if (r) + return r; + + n_dhcp4_client_lease_unlink(lease); + + return 0; +} + +/** + * n_dhcp4_client_lease_decline() - decline an ack'ed lease + * @lease: lease to operate on + * + * Decline a lease. This must be a lease that was ack'ed by the + * server. + * + * The offered IP address must not be used once the lease has been + * decline. + * + * Return: 0 on success, or a negative error code on failure. + */ +_c_public_ int n_dhcp4_client_lease_decline(NDhcp4ClientLease *lease, const char *error) { + int r; + + /* XXX: error handling, this must be an ACK */ + + if (!lease->probe) + return -ENOTRECOVERABLE; + if (lease->probe->current_lease != lease) + return -ENOTRECOVERABLE; + + r = n_dhcp4_client_probe_transition_decline(lease->probe, lease->message, error, n_dhcp4_gettime(CLOCK_BOOTTIME)); + if (r) + return r; + + lease->probe->current_lease = n_dhcp4_client_lease_unref(lease->probe->current_lease); + n_dhcp4_client_lease_unlink(lease); + + return 0; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-c-probe.c b/shared/n-dhcp4/src/n-dhcp4-c-probe.c new file mode 100644 index 000000000..67fedafff --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-c-probe.c @@ -0,0 +1,1202 @@ +/* + * DHCPv4 Client Probes + * + * The probe object is used to represent the lifetime of a DHCP client session. + * A running probe discovers DHCP servers, requests a lease, and maintains that + * lease. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" + + +static int n_dhcp4_client_probe_option_new(NDhcp4ClientProbeOption **optionp, + uint8_t option, + const void *data, + uint8_t n_data) { + NDhcp4ClientProbeOption *op; + + op = malloc(sizeof(op) + n_data); + if (!op) + return -ENOMEM; + + op->option = option; + op->n_data = n_data; + memcpy(op->data, data, n_data); + + *optionp = op; + return 0; +} + +static void n_dhcp4_client_probe_option_free(NDhcp4ClientProbeOption *option) { + if (option) + free(option); +} + +/** + * n_dhcp4_client_probe_config_new() - create new probe configuration + * @configp: output argument to store new configuration + * + * This creates a new probe configuration object. The object is a collection of + * parameters for probes. No data verification is done by the configuration + * object. Instead, when passing the configuration to the constructor of a + * probe, this constructor will perform parameter validation. + * + * A probe configuration is an unlinked object only used to pass information to + * a probe constructor. The caller fully owns the returned configuration object + * and is responsible to free it when no longer needed. + * + * Return: 0 on success, negative error code on failure. + */ +_c_public_ int n_dhcp4_client_probe_config_new(NDhcp4ClientProbeConfig **configp) { + _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL; + + config = calloc(1, sizeof(*config)); + if (!config) + return -ENOMEM; + + *config = (NDhcp4ClientProbeConfig)N_DHCP4_CLIENT_PROBE_CONFIG_NULL(*config); + + *configp = config; + config = NULL; + return 0; +} + +/** + * n_dhcp4_client_probe_config_free() - destroy probe configuration + * @config: configuration to operate on, or NULL + * + * This destroys a probe configuration object and deallocates all its + * resources. + * + * If @config is NULL, this is a no-op. + * + * Return: NULL is returned. + */ +_c_public_ NDhcp4ClientProbeConfig *n_dhcp4_client_probe_config_free(NDhcp4ClientProbeConfig *config) { + if (!config) + return NULL; + + for (unsigned int i = 0; i <= UINT8_MAX; ++i) + n_dhcp4_client_probe_option_free(config->options[i]); + + free(config); + + return NULL; +} + +/** + * n_dhcp4_client_probe_config_dup() - duplicate probe configuration + * @config: configuration to operate on + * @dupp: output argument for duplicate + * + * This duplicates the probe configuration given as @config and returns it in + * @dupp to the caller. + * + * Return: 0 on success, negative error code on failure. + */ +int n_dhcp4_client_probe_config_dup(NDhcp4ClientProbeConfig *config, + NDhcp4ClientProbeConfig **dupp) { + _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *dup = NULL; + int r; + + r = n_dhcp4_client_probe_config_new(&dup); + if (r) + return r; + + dup->inform_only = config->inform_only; + dup->init_reboot = config->init_reboot; + dup->requested_ip = config->requested_ip; + dup->ms_start_delay = config->ms_start_delay; + + for (unsigned int i = 0; i < config->n_request_parameters; ++i) + dup->request_parameters[dup->n_request_parameters++] = config->request_parameters[i]; + + for (unsigned int i = 0; i <= UINT8_MAX; ++i) { + if (!config->options[i]) + break; + + r = n_dhcp4_client_probe_option_new(&dup->options[i], + config->options[i]->option, + config->options[i]->data, + config->options[i]->n_data); + if (r) + return r; + } + + *dupp = dup; + dup = NULL; + return 0; +} + +/** + * n_dhcp4_client_probe_config_set_inform_only() - set inform-only property + * @config: configuration to operate on + * @inform_only: value to set + * + * This sets the inform-only property of the given configuration object. This + * property controls whether the client probe should request a full lease, or + * whether it should just ask for auxiliary information without requesting an + * address. + * + * The default is to request a full lease and address. If inform-only is set to + * true, only auxiliary information will be requested. + * + * XXX: This is currently not implemented, and setting the property has no effect. + */ +_c_public_ void n_dhcp4_client_probe_config_set_inform_only(NDhcp4ClientProbeConfig *config, bool inform_only) { + config->inform_only = inform_only; +} + +/** + * n_dhcp4_client_probe_config_set_init_reboot() - set init-reboot property + * @config: configuration to operate on + * @init_reboot: value to set + * + * This sets the init-reboot property of the given configuration object. If this + * is enabled, a requested IP address must also be set. + * + * The default is false. If set to true, a probe will make use of the + * INIT-REBOOT path, as described by the DHCP specification. In most cases, you + * do not want this. + * + * XXX: This is currently not implemented, and setting the property has no effect. + * + * Background: The INIT-REBOOT path allows a DHCP client to skip + * server-discovery when rebooting/resuming their machine. The DHCP + * client simply re-requests the lease it had acquired before. This + * saves one roundtrip in the success-case, since the DISCOVER step + * is skipped. However, there are little to no timeouts involved, + * so the roundtrip should be barely noticeable. In contrast, if + * the INIT-REBOOT fails (because the lease is no longer valid, or + * not valid on this network), the client has to wait for a + * possible answer to the request before actually starting the DHCP + * process all over. This significantly increases the time needed + * to switch networks. + * The INIT-REBOOT state might have been a real improvements with + * the old resend-timeouts mandated by the DHCP specification. + * However, on modern networks with improved timeout values we + * recommend against using it. + */ +_c_public_ void n_dhcp4_client_probe_config_set_init_reboot(NDhcp4ClientProbeConfig *config, bool init_reboot) { + config->init_reboot = init_reboot; +} + +/** + * n_dhcp4_client_probe_config_set_requested_ip() - set requested-ip property + * @config: configuration to operate on + * @ip: value to set + * + * This sets the requested-ip property of the given configuration object. + * + * The default is all 0. If set to something else, the DHCP discovery will + * include this IP in its requests to tell DHCP servers which address to pick. + * Servers are not required to honor this, nor does this have any effect on + * servers not serving this address. + * + * This field should always be set if the caller knows of an address that was + * previously acquired on this network. It serves as hint to servers and will + * allow them to provide the same address again. + */ +_c_public_ void n_dhcp4_client_probe_config_set_requested_ip(NDhcp4ClientProbeConfig *config, struct in_addr ip) { + config->requested_ip = ip; +} + +/** + * n_dhcp4_client_probe_config_set_start_delay() - set start delay + * @config: configuration to operate on + * @msecs: value to set + * + * This sets the start delay property of the given configuration object. + * + * The default is 9000 ms, which is based on RFC2131. In the RFC the start + * delay is specified to be a random value in the range 1000 to 10.000 ms. + * However, there does not appear to be any particular reason to + * unconditionally wait at least one second, so we move the range down to + * start at 0 ms. The reaon for the random delay is to avoid network-wide + * events causing too much simultaneous network traffic. However, on modern + * networks, a more reasonable value may be in the 10 ms range. + */ +_c_public_ void n_dhcp4_client_probe_config_set_start_delay(NDhcp4ClientProbeConfig *config, uint64_t msecs) { + config->ms_start_delay = msecs; +} + +/** + * n_dhpc4_client_probe_config_request_option() - append option to request from the server + * @config: configuration to operate on + * @option: option to request + * + * This adds an option to the list of options to request from the server. + * + * A server may send options that we do not requst, and it may omit options + * that we do request. However, to increase the likelyhood of uniform behavior + * between server implementations, we do not expose options that were not + * explicitly requested. + * + * When called multiple times, the order matters. Earlier requests are + * considered higher priority than later requests, in case the server must omit + * some, due to a lack of space. If the same option is requested more than once, + * only the first call has an effect. + */ +_c_public_ void n_dhcp4_client_probe_config_request_option(NDhcp4ClientProbeConfig *config, uint8_t option) { + for (unsigned int i = 0; i < config->n_request_parameters; ++i) { + if (config->request_parameters[i] == option) + return; + } + + c_assert(config->n_request_parameters <= UINT8_MAX); + + config->request_parameters[config->n_request_parameters++] = option; +} + +/** + * n_dhcp4_client_probe_config_append_option() - append option to outgoing messages + * @config: configuration to operate on + * @option: DHCP option number + * @data: payload + * @n_data: number of bytes in payload + * + * This sets extra options on a given configuration object. + * + * These options are appended verbatim to outgoing messages where + * that is supported by the specification. The same options are + * appended to all messages. + * + * No option may be appended more than once. Options considered internal + * to the DHCP protocol may not be appended. + * + * Return: 0 on success, N_DHCP4_E_DUPLICATE_OPTION if an option has already been + * appended, N_DHCP4_E_INTERNAL if the option is not configurable, or + * a negative error code on failure. + */ +_c_public_ int n_dhcp4_client_probe_config_append_option(NDhcp4ClientProbeConfig *config, + uint8_t option, + const void *data, + uint8_t n_data) { + int r; + + /* XXX: filter internal options */ + + for (unsigned int i = 0; i <= UINT8_MAX; ++i) { + if (config->options[i]) { + if (config->options[i]->option == option) + return N_DHCP4_E_DUPLICATE_OPTION; + + continue; + } + + r = n_dhcp4_client_probe_option_new(&config->options[i], + option, + data, + n_data); + if (r) + return r; + + return 0; + } + + c_assert(0); + return -ENOTRECOVERABLE; +} + +static void n_dhcp4_client_probe_config_initialize_random_seed(NDhcp4ClientProbeConfig *config) { + uint8_t hash_seed[] = { + 0x25, 0x3f, 0x02, 0x75, 0x3a, 0xb8, 0x4f, 0x91, + 0x9d, 0x0a, 0xd6, 0x15, 0x9d, 0x72, 0x7b, 0xcb, + }; + CSipHash hash = C_SIPHASH_NULL; + unsigned short int seed16v[3]; + const uint8_t *p; + uint64_t u64; + int r; + + /* + * Initialize seed48_r(3) + * + * We need random jitter for all timeouts and delays, used to reduce + * network traffic during bursts. This is not meant as security measure + * but only meant to improve network utilization during bursts. The + * random source is thus negligible. However, we want, under all + * circumstances, avoid two instances running with the same seed. Thus + * we source the seed from AT_RANDOM, which grants us a per-process + * unique seed. We then add the current time to make sure consequetive + * instances use different seeds (to avoid clashes if processes are + * duplicated, or similar), and lastly we add the config memory address + * to avoid clashes of multiple parallel instances. + * + * Again, none of these are meant as security measure, but only to + * avoid *ACCIDENTAL* seed clashes. That is, in the case that many + * transactions are started in parallel, we delay the individual + * messages (as described in the spec), to reduce the traffic on the + * network and the chance of packets being dropped (and thus triggering + * timeouts and resends). + * + * We hash everything through SipHash, to avoid exposing AT_RANDOM and + * other sources to the network. We use a static salt to distinguish it + * from other implementations using the same random source. + */ + c_siphash_init(&hash, hash_seed); + + p = (const uint8_t *)getauxval(AT_RANDOM); + if (p) + c_siphash_append(&hash, p, 16); + + u64 = n_dhcp4_gettime(CLOCK_MONOTONIC); + c_siphash_append(&hash, (const uint8_t *)&u64, sizeof(u64)); + + c_siphash_append(&hash, (const uint8_t *)&config, sizeof(config)); + + u64 = c_siphash_finalize(&hash); + + seed16v[0] = (u64 >> 0) ^ (u64 >> 48); + seed16v[1] = (u64 >> 16) ^ (u64 >> 0); + seed16v[2] = (u64 >> 32) ^ (u64 >> 16); + + r = seed48_r(seed16v, &config->entropy); + c_assert(!r); +} + +/** + * n_dhcp4_client_probe_config_get_random() - get random data + * @config: config object to operate on + * + * Fetch the next 32bit random number from the entropy pool in @config. + * Note that this is in no way suitable for security purposes. + * + * Return: the random data. + */ +uint32_t n_dhcp4_client_probe_config_get_random(NDhcp4ClientProbeConfig *config) { + long int result; + int r; + + r = mrand48_r(&config->entropy, &result); + c_assert(!r); + + return result; +}; + +/** + * n_dhcp4_client_probe_new() - create new client probe + * @probep: output argument for new client probe + * @config: probe configuration + * @client: client to probe on behalf of + * @ns_now: the current time + * + * This creates a new client probe object. + * + * If one is already running, the new one will be immediately (but asynchronously) + * cancelled. Otherwise, a DISCOVER event is scheduled after a randomized delay. + * + * Return: 0 on success, or a negative error code on failure. + */ +int n_dhcp4_client_probe_new(NDhcp4ClientProbe **probep, + NDhcp4ClientProbeConfig *config, + NDhcp4Client *client, + uint64_t ns_now) { + _c_cleanup_(n_dhcp4_client_probe_freep) NDhcp4ClientProbe *probe = NULL; + bool active; + int r; + + /* + * If there is already a probe attached, we create the new probe in + * detached state. It will not be linked into the epoll context and not + * be useful in any way. We immediately raise the CANCELLED event to + * notify the caller about it. + */ + active = !client->current_probe; + + probe = calloc(1, sizeof(*probe)); + if (!probe) + return -ENOMEM; + + *probe = (NDhcp4ClientProbe)N_DHCP4_CLIENT_PROBE_NULL(*probe); + probe->client = n_dhcp4_client_ref(client); + + r = n_dhcp4_client_probe_config_dup(config, &probe->config); + if (r) + return r; + + /* + * XXX: make seed initialization optional, so the entropy can be reused. + */ + n_dhcp4_client_probe_config_initialize_random_seed(probe->config); + + r = n_dhcp4_c_connection_init(&probe->connection, + client->config, + probe->config, + active ? client->fd_epoll : -1); + if (r) + return r; + + if (active) { + /* + * Defer the sending of DISCOVER by a random amount (by default up to 9 seconds). + */ + probe->ns_deferred = ns_now + (n_dhcp4_client_probe_config_get_random(probe->config) % (probe->config->ms_start_delay * 1000000ULL)); + probe->client->current_probe = probe; + } else { + r = n_dhcp4_client_probe_raise(probe, + NULL, + N_DHCP4_CLIENT_EVENT_CANCELLED); + if (r) + return r; + } + + *probep = probe; + probe = NULL; + return 0; +} + +/** + * n_dhcp4_client_probe_free() - destroy a probe + * @probe: probe to operate on, or NULL + * + * This destroys a probe object and deallocates all its resources. + * + * If @probe is NULL, this is a no-op. + * + * Return: NULL is returned. + */ +_c_public_ NDhcp4ClientProbe *n_dhcp4_client_probe_free(NDhcp4ClientProbe *probe) { + NDhcp4CEventNode *node, *t_node; + NDhcp4ClientLease *lease, *t_lease; + + if (!probe) + return NULL; + + c_list_for_each_entry_safe(lease, t_lease, &probe->lease_list, probe_link) + n_dhcp4_client_lease_unlink(lease); + + c_list_for_each_entry_safe(node, t_node, &probe->event_list, probe_link) + n_dhcp4_c_event_node_free(node); + + if (probe == probe->client->current_probe) + probe->client->current_probe = NULL; + + n_dhcp4_c_connection_deinit(&probe->connection); + n_dhcp4_client_unref(probe->client); + n_dhcp4_client_probe_config_free(probe->config); + + c_assert(c_list_is_empty(&probe->lease_list)); + c_assert(c_list_is_empty(&probe->event_list)); + free(probe); + + return NULL; +} + +/** + * n_dhcp4_client_probe_set_userdata() - set userdata pointer + * @probe: the probe to operate on + * @userdata: pointer to userdata + * + * Set a userdata pointer. The pointed to data is still owned by the caller, and + * is completely opaque to the probe. + */ +_c_public_ void n_dhcp4_client_probe_set_userdata(NDhcp4ClientProbe *probe, void *userdata) { + probe->userdata = userdata; +} + +/** + * n_dhcp4_client_probe_get_userdata() - get userdata pointer + * @probe: the probe to operate on + * @userdatap: return pointer for userdata pointer + * + * Get the userdata pointer. The lifetime of the userdata and making sure it is + * still valid when accessed via the probe is the responsibility of the caller. + */ +_c_public_ void n_dhcp4_client_probe_get_userdata(NDhcp4ClientProbe *probe, void **userdatap) { + *userdatap = probe->userdata; +} + +/** + * n_dhcp4_client_probe_raise() - XXX + */ +int n_dhcp4_client_probe_raise(NDhcp4ClientProbe *probe, NDhcp4CEventNode **nodep, unsigned int event) { + NDhcp4CEventNode *node; + int r; + + r = n_dhcp4_client_raise(probe->client, &node, event); + if (r) + return r; + + switch (event) { + case N_DHCP4_CLIENT_EVENT_OFFER: + node->event.offer.probe = probe; + break; + case N_DHCP4_CLIENT_EVENT_GRANTED: + node->event.granted.probe = probe; + break; + case N_DHCP4_CLIENT_EVENT_RETRACTED: + node->event.retracted.probe = probe; + break; + case N_DHCP4_CLIENT_EVENT_EXTENDED: + node->event.extended.probe = probe; + break; + case N_DHCP4_CLIENT_EVENT_EXPIRED: + node->event.expired.probe = probe; + break; + case N_DHCP4_CLIENT_EVENT_CANCELLED: + node->event.cancelled.probe = probe; + break; + default: + c_assert(0); + n_dhcp4_c_event_node_free(node); + return -ENOTRECOVERABLE; + } + + if (nodep) + *nodep = node; + return 0; +} + +void n_dhcp4_client_probe_get_timeout(NDhcp4ClientProbe *probe, uint64_t *timeoutp) { + uint64_t t1 = 0; + uint64_t t2 = 0; + uint64_t lifetime = 0; + uint64_t timeout = 0; + + if (probe->current_lease) { + t1 = probe->current_lease->t1; + t2 = probe->current_lease->t2; + lifetime = probe->current_lease->lifetime; + } + + n_dhcp4_c_connection_get_timeout(&probe->connection, &timeout); + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + if (probe->ns_deferred && (!timeout || probe->ns_deferred < timeout)) + timeout = probe->ns_deferred; + + break; + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + if (t1 && (!timeout || t1 < timeout)) + timeout = t1; + + /* fall-through */ + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + if (t2 && (!timeout || t2 < timeout)) + timeout = t2; + + /* fall-through */ + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + if (lifetime && (!timeout || lifetime < timeout)) + timeout = lifetime; + break; + default: + /* ignore */ + break; + } + + *timeoutp = timeout; +} + +static int n_dhcp4_client_probe_outgoing_append_options(NDhcp4ClientProbe *probe, NDhcp4Outgoing *outgoing) { + int r; + + for (unsigned int i = 0; i <= UINT8_MAX; ++i) { + if (!probe->config->options[i]) + break; + + r = n_dhcp4_outgoing_append(outgoing, + probe->config->options[i]->option, + probe->config->options[i]->data, + probe->config->options[i]->n_data); + if (r) { + if (r == N_DHCP4_E_NO_SPACE) + /* XXX */ + break; + + return r; + } + } + + return 0; +} + +static int n_dhcp4_client_probe_transition_deferred(NDhcp4ClientProbe *probe, uint64_t ns_now) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + r = n_dhcp4_c_connection_listen(&probe->connection); + if (r) + return r; + + r = n_dhcp4_c_connection_discover_new(&probe->connection, &request); + if (r) + return r; + + if (probe->config->requested_ip.s_addr != INADDR_ANY) { + r = n_dhcp4_outgoing_append_requested_ip(request, probe->config->requested_ip); + if (r) + return r; + } + + r = n_dhcp4_client_probe_outgoing_append_options(probe, request); + if (r) + return r; + + r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now); + if (r) + return r; + else + request = NULL; /* consumed */ + + probe->state = N_DHCP4_CLIENT_PROBE_STATE_SELECTING; + probe->ns_deferred = 0; + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + abort(); + break; + } + + return 0; +} + +static int n_dhcp4_client_probe_transition_t1(NDhcp4ClientProbe *probe, uint64_t ns_now) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + r = n_dhcp4_c_connection_renew_new(&probe->connection, &request); + if (r) + return r; + + r = n_dhcp4_client_probe_outgoing_append_options(probe, request); + if (r) + return r; + + r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now); + if (r) + return r; + else + request = NULL; /* consumed */ + + probe->state = N_DHCP4_CLIENT_PROBE_STATE_RENEWING; + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + abort(); + break; + } + + return 0; +} + +static int n_dhcp4_client_probe_transition_t2(NDhcp4ClientProbe *probe, uint64_t ns_now) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + r = n_dhcp4_c_connection_rebind_new(&probe->connection, &request); + if (r) + return r; + + r = n_dhcp4_client_probe_outgoing_append_options(probe, request); + if (r) + return r; + + r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now); + if (r) + return r; + else + request = NULL; /* consumed */ + + probe->state = N_DHCP4_CLIENT_PROBE_STATE_REBINDING; + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + abort(); + break; + } + + return 0; +} + +static int n_dhcp4_client_probe_transition_lifetime(NDhcp4ClientProbe *probe) { + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + + /* XXX */ + + r = n_dhcp4_client_probe_raise(probe, + NULL, + N_DHCP4_CLIENT_EVENT_EXPIRED); + if (r) + return r; + + c_assert(probe->client->current_probe == probe); + probe->client->current_probe = NULL; + + n_dhcp4_c_connection_close(&probe->connection); + + probe->state = N_DHCP4_CLIENT_PROBE_STATE_EXPIRED; + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + abort(); + break; + } + + return 0; +} + +static int n_dhcp4_client_probe_transition_offer(NDhcp4ClientProbe *probe, NDhcp4Incoming *message) { + _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL; + NDhcp4CEventNode *node; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + + r = n_dhcp4_client_probe_raise(probe, + &node, + N_DHCP4_CLIENT_EVENT_OFFER); + if (r) + return r; + + r = n_dhcp4_client_lease_new(&lease, message); + if (r) + return r; + + /* message consumed, do not fail */ + + n_dhcp4_client_lease_link(lease, probe); + + node->event.offer.lease = n_dhcp4_client_lease_ref(lease); + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + /* ignore */ + break; + } + + return 0; +} + +static int n_dhcp4_client_probe_transition_ack(NDhcp4ClientProbe *probe, NDhcp4Incoming *message) { + _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL; + NDhcp4CEventNode *node; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + + r = n_dhcp4_client_probe_raise(probe, + &node, + N_DHCP4_CLIENT_EVENT_EXTENDED); + if (r) + return r; + + r = n_dhcp4_client_lease_new(&lease, message); + if (r) + return r; + + /* message consumed, do not fail */ + + n_dhcp4_client_lease_link(lease, probe); + + node->event.extended.lease = n_dhcp4_client_lease_ref(lease); + n_dhcp4_client_lease_unref(probe->current_lease); + probe->current_lease = n_dhcp4_client_lease_ref(lease); + probe->state = N_DHCP4_CLIENT_PROBE_STATE_BOUND; + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + + r = n_dhcp4_client_probe_raise(probe, + &node, + N_DHCP4_CLIENT_EVENT_GRANTED); + if (r) + return r; + + r = n_dhcp4_client_lease_new(&lease, message); + if (r) + return r; + + /* message consumed, don to fail */ + + n_dhcp4_client_lease_link(lease, probe); + + node->event.granted.lease = n_dhcp4_client_lease_ref(lease); + probe->current_lease = n_dhcp4_client_lease_ref(lease); + probe->state = N_DHCP4_CLIENT_PROBE_STATE_GRANTED; + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + /* ignore */ + break; + } + + return 0; +} + +static int n_dhcp4_client_probe_transition_nak(NDhcp4ClientProbe *probe) { + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + + /* XXX */ + + r = n_dhcp4_client_probe_raise(probe, + NULL, + N_DHCP4_CLIENT_EVENT_RETRACTED); + if (r) + return r; + + probe->state = N_DHCP4_CLIENT_PROBE_STATE_INIT; + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + /* ignore */ + break; + } + + return 0; +} + +int n_dhcp4_client_probe_transition_select(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, uint64_t ns_now) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + r = n_dhcp4_c_connection_select_new(&probe->connection, &request, offer); + if (r) + return r; + + r = n_dhcp4_client_probe_outgoing_append_options(probe, request); + if (r) + return r; + + r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now); + if (r) + return r; + else + request = NULL; /* consumed */ + + /* XXX: ignore other offers */ + + probe->state = N_DHCP4_CLIENT_PROBE_STATE_REQUESTING; + + break; + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + /* ignore */ + break; + } + + return 0; +} + +/** + * n_dhcp4_client_probe_transition_accept() - XXX + */ +int n_dhcp4_client_probe_transition_accept(NDhcp4ClientProbe *probe, NDhcp4Incoming *ack) { + struct in_addr client = {}; + struct in_addr server = {}; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + n_dhcp4_incoming_get_yiaddr(ack, &client); + + r = n_dhcp4_incoming_query_server_identifier(ack, &server); + if (r) + return r; + + r = n_dhcp4_c_connection_connect(&probe->connection, &client, &server); + if (r) + return r; + + probe->state = N_DHCP4_CLIENT_PROBE_STATE_BOUND; + + /* XXX: trigger timers */ + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + /* ignore */ + break; + } + + return 0; +} + +/** + * n_dhc4_client_probe_transition_decline() - XXX + */ +int n_dhcp4_client_probe_transition_decline(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, const char *error, uint64_t ns_now) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL; + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + r = n_dhcp4_c_connection_decline_new(&probe->connection, &request, offer, error); + if (r) + return r; + + r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now); + if (r) + return r; + else + request = NULL; /* consumed */ + + /* XXX: what state to transition to? */ + + break; + + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT: + case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING: + case N_DHCP4_CLIENT_PROBE_STATE_SELECTING: + case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING: + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED: + default: + /* ignore */ + break; + } + + return 0; +} + +/** + * n_dhcp4_client_probe_dispatch_timer() - XXX + */ +int n_dhcp4_client_probe_dispatch_timer(NDhcp4ClientProbe *probe, uint64_t ns_now) { + int r; + + switch (probe->state) { + case N_DHCP4_CLIENT_PROBE_STATE_INIT: + if (ns_now >= probe->ns_deferred) { + r = n_dhcp4_client_probe_transition_deferred(probe, ns_now); + if (r) + return r; + } + + break; + case N_DHCP4_CLIENT_PROBE_STATE_GRANTED: + if (ns_now >= probe->current_lease->lifetime) { + r = n_dhcp4_client_probe_transition_lifetime(probe); + if (r) + return r; + } + + break; + case N_DHCP4_CLIENT_PROBE_STATE_BOUND: + case N_DHCP4_CLIENT_PROBE_STATE_RENEWING: + case N_DHCP4_CLIENT_PROBE_STATE_REBINDING: + if (ns_now >= probe->current_lease->lifetime) { + r = n_dhcp4_client_probe_transition_lifetime(probe); + if (r) + return r; + } else if (ns_now >= probe->current_lease->t2) { + r = n_dhcp4_client_probe_transition_t2(probe, ns_now); + if (r) + return r; + } else if (ns_now >= probe->current_lease->t1) { + r = n_dhcp4_client_probe_transition_t1(probe, ns_now); + if (r) + return r; + } + + break; + default: + /* ignore */ + break; + } + + r = n_dhcp4_c_connection_dispatch_timer(&probe->connection, ns_now); + if (r) + return r; + + return 0; +} + +/** + * n_dhcp4_client_probe_dispatch_connection() - XXX + */ +int n_dhcp4_client_probe_dispatch_io(NDhcp4ClientProbe *probe, uint32_t events) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + uint8_t type; + int r; + + r = n_dhcp4_c_connection_dispatch_io(&probe->connection, &message); + if (r) { + if (r == N_DHCP4_E_AGAIN) + return 0; + else if (r == N_DHCP4_E_MALFORMED || r == N_DHCP4_E_UNEXPECTED) { + /* + * We fetched something from the sockets, which we + * discarded. We don't know whether there is more data + * to fetch, so we set the preempted flag to notify the + * caller we want to be called again. + */ + probe->client->preempted = true; + return 0; + } + + return r; + } + + /* + * We fetched something from the sockets, which we will handle below. + * We don't know whether there is more data to fetch, so we set the + * preempted flag to notify the caller we want to be called again. + */ + probe->client->preempted = true; + + r = n_dhcp4_incoming_query_message_type(message, &type); + if (r == N_DHCP4_E_UNSET || r == N_DHCP4_E_MALFORMED) + /* + * XXX: this can never happen as we already queried the message + * type. + */ + return 0; + + switch (type) { + case N_DHCP4_MESSAGE_OFFER: + r = n_dhcp4_client_probe_transition_offer(probe, message); + if (r) + return r; + else + message = NULL; /* consumed */ + break; + case N_DHCP4_MESSAGE_ACK: + r = n_dhcp4_client_probe_transition_ack(probe, message); + if (r) + return r; + else + message = NULL; /* consumed */ + break; + case N_DHCP4_MESSAGE_NAK: + r = n_dhcp4_client_probe_transition_nak(probe); + if (r) + return r; + break; + default: + /* + * We receiveda message type we do not support, simply discard + * it. + */ + break; + } + + return 0; +} + +/** + * n_dhcp4_client_probe_update_mtu() - XXX + */ +int n_dhcp4_client_probe_update_mtu(NDhcp4ClientProbe *probe, uint16_t mtu) { + return 0; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-client.c b/shared/n-dhcp4/src/n-dhcp4-client.c new file mode 100644 index 000000000..a492a74c6 --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-client.c @@ -0,0 +1,831 @@ +/* + * Client Side of the Dynamic Host Configuration Protocol for IPv4 + * + * This implements the public API around the NDhcp4Client object. The client + * object is simply a context to track running probes. It manages pending + * events of all probes, as well as forwards the dispatching requests whenever + * the dispatcher is run. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" + +/** + * n_dhcp4_client_config_new() - allocate new client configuration + * @configp: output argument for new client config + * + * This creates a new client configuration object. Client configurations are + * unlinked objects that merely serve as collection of parameters. They do not + * perform validity checks. + * + * The new client configuration is fully owned by the caller. They are + * responsible to free the object if no longer needed. + * + * Return: 0 on success, negative error code on failure. + */ +_c_public_ int n_dhcp4_client_config_new(NDhcp4ClientConfig **configp) { + _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL; + + config = calloc(1, sizeof(*config)); + if (!config) + return -ENOMEM; + + *config = (NDhcp4ClientConfig)N_DHCP4_CLIENT_CONFIG_NULL(*config); + + *configp = config; + config = NULL; + return 0; +} + +/** + * n_dhcp4_client_config_free() - destroy client configuration + * @config: client configuration to operate on, or NULL + * + * This destroys a client configuration and deallocates all its resources. If + * NULL is passed, this is a no-op. + * + * Return: NULL is returned. + */ +_c_public_ NDhcp4ClientConfig *n_dhcp4_client_config_free(NDhcp4ClientConfig *config) { + if (!config) + return NULL; + + free(config->client_id); + free(config); + + return NULL; +} + +/** + * n_dhcp4_client_config_dup() - duplicate client configuration + * @config: client configuration to operate on + * @dupp: output argument for duplicate + * + * This duplicates the client configuration given as @config and returns it in + * @dupp to the caller. + * + * Return: 0 on success, negative error code on failure. + */ +int n_dhcp4_client_config_dup(NDhcp4ClientConfig *config, NDhcp4ClientConfig **dupp) { + _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *dup = NULL; + int r; + + r = n_dhcp4_client_config_new(&dup); + if (r) + return r; + + dup->ifindex = config->ifindex; + dup->transport = config->transport; + dup->request_broadcast = config->request_broadcast; + memcpy(dup->mac, config->mac, sizeof(dup->mac)); + dup->n_mac = config->n_mac; + memcpy(dup->broadcast_mac, config->broadcast_mac, sizeof(dup->broadcast_mac)); + dup->n_broadcast_mac = config->n_broadcast_mac; + + r = n_dhcp4_client_config_set_client_id(dup, + config->client_id, + config->n_client_id); + if (r) + return r; + + *dupp = dup; + dup = NULL; + return 0; +} + +/** + * n_dhcp4_client_config_set_ifindex() - set ifindex property + * @config: client configuration to operate on + * @ifindex: ifindex to set + * + * This sets the ifindex property of the client configuration. The ifindex + * specifies the network device that a DHCP client will run on. + */ +_c_public_ void n_dhcp4_client_config_set_ifindex(NDhcp4ClientConfig *config, int ifindex) { + config->ifindex = ifindex; +} + +/** + * n_dhcp4_client_config_set_transport() - set transport property + * @config: client configuration to operate on + * @transport: transport to set + * + * This sets the transport property of the client configuration. The transport + * defines the hardware transport of the network device that a DHCP client + * runs on. + * + * This takes one of the N_DHCP4_TRANSPORT_* identifiers as argument. + */ +_c_public_ void n_dhcp4_client_config_set_transport(NDhcp4ClientConfig *config, unsigned int transport) { + config->transport = transport; +} + +/** + * n_dhcp4_client_config_set_request_broadcast() - set request-broadcast property + * @config: configuration to operate on + * @request_broadcast: value to set + * + * This sets the request_broadcast property of the given configuration object. + * + * The default is false. If set to true, a the server will be told to not unicast + * replies to the client's IP address before it has been configured, but broadcast + * to INADDR_ANY instead. In most cases, you do not want this. + * + * Background: OFFER and ACK messages from DHCP servers to clients are unicast + * to the IP address handed out, even before the IP address has + * been configured on the taregt interface. This usually works + * because the correct destination hardware address is explicitly + * set on the outgoing packets, rather than being resolved (which + * would not work). However, some hardware does not accept incoming + * IP packets destined for addresses they do not own, even if the + * hardware address is correct. In this case, the server must + * broadcast the replies in order for the client to receive them. + * In general, unneccesary broadcasting is something one wants to + * avoid, and some networks will not deliver broadcasts to the + * client at all, in which case this flag must not be set. + */ +_c_public_ void n_dhcp4_client_config_set_request_broadcast(NDhcp4ClientConfig *config, bool request_broadcast) { + config->request_broadcast = request_broadcast; +} + +/** + * n_dhcp4_client_config_set_mac() - set mac property + * @config: client configuration to operate on + * @mac: hardware address to set + * @n_mac: length of the hardware address + * + * This sets the mac property of the client configuration. It specifies the + * hardware address of the local interface that the DHCP client runs on. + * + * This function copies the specified hardware address into @config. Any + * hardware address is supported. It is up to the consumer of the client + * configuration to verify the validity of the hardware address. + * + * Note: This function may truncate the hardware address internally, but + * retains the original length. The consumer of this configuration can + * thus tell whether the data was truncated and will refuse it. + * The internal buffer is big enough to hold any hardware address of all + * supported transports. Thus, truncation only happens if you use + * unsupported transports, and those will be rejected, anyway. + */ +_c_public_ void n_dhcp4_client_config_set_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac) { + config->n_mac = n_mac; + memcpy(config->mac, mac, c_min(n_mac, sizeof(config->mac))); +} + +/** + * n_dhcp4_client_config_set_broadcast_mac() - set broadcast-mac property + * @config: client configuration to operate on + * @mac: hardware address to set + * @n_mac: length of the hardware address + * + * This sets the broadcast-mac property of the client configuration. It + * specifies the destination hardware address to use for broadcasts on the + * local interface that the DHCP client runs on. + * + * This function copies the specified hardware address into @config. Any + * hardware address is supported. It is up to the consumer of the client + * configuration to verify the validity of the hardware address. + * + * Note: This function may truncate the hardware address internally, but + * retains the original length. The consumer of this configuration can + * thus tell whether the data was truncated and will refuse it. + * The internal buffer is big enough to hold any hardware address of all + * supported transports. Thus, truncation only happens if you use + * unsupported transports, and those will be rejected, anyway. + */ +_c_public_ void n_dhcp4_client_config_set_broadcast_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac) { + config->n_broadcast_mac = n_mac; + memcpy(config->broadcast_mac, mac, c_min(n_mac, sizeof(config->broadcast_mac))); +} + +/** + * n_dhcp4_client_config_set_client_id() - set client-id property + * @config: client configuration to operate on + * @id: client id + * @n_id: length of the client id in bytes + * + * This sets the client-id property of @config. It copies the entire client-id + * buffer into the configuration. + * + * Return: 0 on success, negative error code on failure. + */ +_c_public_ int n_dhcp4_client_config_set_client_id(NDhcp4ClientConfig *config, const uint8_t *id, size_t n_id) { + uint8_t *t; + + t = malloc(n_id + 1); + if (!t) + return -ENOMEM; + + free(config->client_id); + config->client_id = t; + config->n_client_id = n_id; + + memcpy(config->client_id, id, n_id); + config->client_id[n_id] = 0; /* safety 0 for debugging */ + + return 0; +} + +/** + * n_dhcp4_c_event_node_new() - allocate new event + * @nodep: output argument for new event + * + * This allocates a new event node and returns it to the caller. The caller + * fully owns the event-node and is reposonsible to either link it somewhere, + * or release it. + * + * Event nodes can be linked on a client object, as well as optionally on a + * probe object. As long as an event-node is linked, it will be retrievable by + * the API user through n_dhcp4_client_pop_event(). Furthermore, destruction of + * the client, or probe respectively, will clean-up all pending events. + * + * Return: 0 on success, negative error code on failure. + */ +int n_dhcp4_c_event_node_new(NDhcp4CEventNode **nodep) { + NDhcp4CEventNode *node; + + node = calloc(1, sizeof(*node)); + if (!node) + return -ENOMEM; + + *node = (NDhcp4CEventNode)N_DHCP4_C_EVENT_NODE_NULL(*node); + + *nodep = node; + return 0; +} + +/** + * n_dhcp4_c_event_node_free() - deallocate event + * @node: node to operate on, or NULL + * + * This deallocates the node given as @node. If the node is linked on a client + * or probe, it is unlinked automatically. + * + * If @probe is NULL, this is a no-op. + * + * Return: NULL is returned. + */ +NDhcp4CEventNode *n_dhcp4_c_event_node_free(NDhcp4CEventNode *node) { + if (!node) + return NULL; + + switch (node->event.event) { + case N_DHCP4_CLIENT_EVENT_OFFER: + node->event.offer.lease = n_dhcp4_client_lease_unref(node->event.offer.lease); + break; + case N_DHCP4_CLIENT_EVENT_GRANTED: + node->event.granted.lease = n_dhcp4_client_lease_unref(node->event.granted.lease); + break; + case N_DHCP4_CLIENT_EVENT_EXTENDED: + node->event.extended.lease = n_dhcp4_client_lease_unref(node->event.extended.lease); + break; + default: + break; + } + + c_list_unlink(&node->probe_link); + c_list_unlink(&node->client_link); + free(node); + + return NULL; +} + +/** + * n_dhcp4_client_new() - allocate new client + * @clientp: output argument for new client + * @config: configuration to use + * + * This allocates a new DHCP4 client object and returns it in @clientp to the + * caller. The caller then owns a single ref-count to the object and is + * responsible to drop it, when no longer needed. + * + * The configuration given as @config is used to initialize the client. The + * caller is free to destroy the configuration once this function returns. + * + * Return: 0 on success, negative error code on failure. + */ +_c_public_ int n_dhcp4_client_new(NDhcp4Client **clientp, NDhcp4ClientConfig *config) { + _c_cleanup_(n_dhcp4_client_unrefp) NDhcp4Client *client = NULL; + struct epoll_event ev = { + .events = EPOLLIN, + }; + int r; + + c_assert(clientp); + + /* verify configuration */ + { + if (config->ifindex < 1) + return N_DHCP4_E_INVALID_IFINDEX; + + switch (config->transport) { + case N_DHCP4_TRANSPORT_ETHERNET: + if (config->n_mac != ETH_ALEN || + config->n_broadcast_mac != ETH_ALEN) + return N_DHCP4_E_INVALID_ADDRESS; + + break; + case N_DHCP4_TRANSPORT_INFINIBAND: + if (config->n_mac != INFINIBAND_ALEN || + config->n_broadcast_mac != INFINIBAND_ALEN) + return N_DHCP4_E_INVALID_ADDRESS; + + break; + default: + return N_DHCP4_E_INVALID_TRANSPORT; + } + + if (config->n_client_id < 1) + return N_DHCP4_E_INVALID_CLIENT_ID; + } + + client = malloc(sizeof(*client)); + if (!client) + return -ENOMEM; + + *client = (NDhcp4Client)N_DHCP4_CLIENT_NULL(*client); + + r = n_dhcp4_client_config_dup(config, &client->config); + if (r) + return r; + + client->fd_epoll = epoll_create1(EPOLL_CLOEXEC); + if (client->fd_epoll < 0) + return -errno; + + client->fd_timer = timerfd_create(CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK); + if (client->fd_timer < 0) + return -errno; + + ev.data.u32 = N_DHCP4_CLIENT_EPOLL_TIMER; + r = epoll_ctl(client->fd_epoll, EPOLL_CTL_ADD, client->fd_timer, &ev); + if (r < 0) + return -errno; + + *clientp = client; + client = NULL; + return 0; +} + +static void n_dhcp4_client_free(NDhcp4Client *client) { + NDhcp4CEventNode *node, *t_node; + + c_assert(!client->current_probe); + + c_list_for_each_entry_safe(node, t_node, &client->event_list, client_link) + n_dhcp4_c_event_node_free(node); + + if (client->fd_timer >= 0) { + epoll_ctl(client->fd_epoll, EPOLL_CTL_DEL, client->fd_timer, NULL); + close(client->fd_timer); + } + + if (client->fd_epoll >= 0) + close(client->fd_epoll); + + n_dhcp4_client_config_free(client->config); + free(client); +} + +/** + * n_dhcp4_client_ref() - acquire client reference + * @client: client to operate on, or NULL + * + * This acquires a reference to the client given as @client. If @client is + * NULL, this function is a no-op. + * + * Return: @client is returned. + */ +_c_public_ NDhcp4Client *n_dhcp4_client_ref(NDhcp4Client *client) { + if (client) + ++client->n_refs; + return client; +} + +/** + * n_dhcp4_client_unref() - release client reference + * @client: client to operate on, or NULL + * + * This releases a reference to the client given as @client. If @client is + * NULL, this is a no-op. + * + * Once the last reference is dropped, the client object will get destroyed and + * deallocated. + * + * Return: NULL is returned. + */ +_c_public_ NDhcp4Client *n_dhcp4_client_unref(NDhcp4Client *client) { + if (client && !--client->n_refs) + n_dhcp4_client_free(client); + return NULL; +} + +/** + * n_dhcp4_client_raise() - raise event + * @client: client to operate on + * @nodep: output argument for new event, or NULL + * @event: event type to use + * + * This creates a new event-node on @client, setting the event-type to @event. + * The newly created event-node is returned to the caller in @nodep (unless + * @nodep is NULL). + * + * The event-node is automatically linked on @client. + * + * Return: 0 on success, negative error code on failure. + */ +int n_dhcp4_client_raise(NDhcp4Client *client, NDhcp4CEventNode **nodep, unsigned int event) { + NDhcp4CEventNode *node; + int r; + + r = n_dhcp4_c_event_node_new(&node); + if (r) + return r; + + node->event.event = event; + c_list_link_tail(&client->event_list, &node->client_link); + + if (nodep) + *nodep = node; + return 0; +} + +/** + * n_dhcp4_client_arm_timer() - update timer + * @client: client to operate on + * + * This updates the timer on @client to fire on the next pending timeout. This + * must be called whenever a timeout on @client might have changed. + */ +void n_dhcp4_client_arm_timer(NDhcp4Client *client) { + uint64_t timeout = 0; + int r; + + if (client->current_probe) + n_dhcp4_client_probe_get_timeout(client->current_probe, &timeout); + + if (timeout != client->scheduled_timeout) { + r = timerfd_settime(client->fd_timer, + TFD_TIMER_ABSTIME, + &(struct itimerspec){ + .it_value = { + .tv_sec = timeout / UINT64_C(1000000000), + .tv_nsec = timeout % UINT64_C(1000000000), + }, + }, + NULL); + c_assert(r >= 0); + + client->scheduled_timeout = timeout; + } +} + +/** + * n_dhcp4_client_get_fd() - retrieve event FD + * @client: client to operate on + * @fdp: output argument to store FD + * + * This retrieves the FD used by the client object given as @client. The FD is + * always valid, and returned in @fdp. + * + * The caller is expected to poll this FD for readable events and call + * n_dhcp4_client_dispatch() whenever the FD is readable. + */ +_c_public_ void n_dhcp4_client_get_fd(NDhcp4Client *client, int *fdp) { + *fdp = client->fd_epoll; +} + +static int n_dhcp4_client_dispatch_timer(NDhcp4Client *client, struct epoll_event *event) { + uint64_t v, ns_now; + int r; + + if (event->events & (EPOLLHUP | EPOLLERR)) { + /* + * There is no way to handle either gracefully. If we ignored + * them, we would busy-loop, so lets rather forward the error + * to the caller. + */ + return -ENOTRECOVERABLE; + } + + if (event->events & EPOLLIN) { + r = read(client->fd_timer, &v, sizeof(v)); + if (r < 0) { + if (errno == EAGAIN) { + /* + * There are no more pending events, so nothing + * to be done. Return to the caller. + */ + return 0; + } + + /* + * Something failed. We use CLOCK_BOOTTIME/MONOTONIC, + * so ECANCELED cannot happen. Hence, there is no error + * that we could gracefully handle. Fail hard and let + * the caller deal with it. + */ + return -errno; + } else if (r != sizeof(v) || v == 0) { + /* + * Kernel guarantees 8-byte reads, and only to return + * data if at least one timer triggered; fail hard if + * it suddenly starts exposing unexpected behavior. + */ + return -ENOTRECOVERABLE; + } + + /* + * Forward the timer-event to the active probe. Timers should + * not fire if there is no probe running, but lets ignore them + * for now, so probe-internals are not leaked to this generic + * client dispatcher. + */ + if (client->current_probe) { + /* + * Read the current time *after* dispatching the timer, + * to make sure we do not miss wakeups. + */ + ns_now = n_dhcp4_gettime(CLOCK_BOOTTIME); + + r = n_dhcp4_client_probe_dispatch_timer(client->current_probe, + ns_now); + if (r) + return r; + } + } + + return 0; +} + +static int n_dhcp4_client_dispatch_io(NDhcp4Client *client, struct epoll_event *event) { + int r; + + if (client->current_probe) + r = n_dhcp4_client_probe_dispatch_io(client->current_probe, + event->events); + else + return -ENOTRECOVERABLE; + + return r; +} + +/** + * n_dhcp4_client_dispatch() - dispatch client + * @client: client to operate on + * + * This dispatches pending operations on @client. It will read incoming + * messages, write pending data, and handle any timeouts. + * + * This function never blocks. + * + * If there are more events to dispatch, than would be reasonable to do in a + * single dispatch, this will return N_DHCP4_E_PREEMPTED. In this case the + * caller is expected to call into this function again when it is ready to + * dispatch more events. + * If your event loop is level-triggered (it very likely is), you can + * optionally ignore this return code and treat it as success. + * + * Return: 0 on success, negative error code on failure, N_DHCP4_E_PREEMPTED if + * there is more data to dispatch. + */ +_c_public_ int n_dhcp4_client_dispatch(NDhcp4Client *client) { + struct epoll_event events[2]; + int n, i, r = 0; + + n = epoll_wait(client->fd_epoll, events, sizeof(events) / sizeof(*events), 0); + if (n < 0) { + /* Linux never returns EINTR if `timeout == 0'. */ + return -errno; + } + + client->preempted = false; + + for (i = 0; i < n; ++i) { + switch (events[i].data.u32) { + case N_DHCP4_CLIENT_EPOLL_TIMER: + r = n_dhcp4_client_dispatch_timer(client, events + i); + break; + case N_DHCP4_CLIENT_EPOLL_IO: + r = n_dhcp4_client_dispatch_io(client, events + i); + break; + default: + c_assert(0); + r = 0; + break; + } + + if (r) { + if (r == N_DHCP4_E_DOWN) { + r = n_dhcp4_client_raise(client, + NULL, + N_DHCP4_CLIENT_EVENT_DOWN); + if (r) + return r; + + /* continue normally */ + } else if (r) { + c_assert(r < _N_DHCP4_E_INTERNAL); + return r; + } + } + } + + n_dhcp4_client_arm_timer(client); + + return client->preempted ? N_DHCP4_E_PREEMPTED : 0; +} + +/** + * n_dhcp4_client_pop_event() - fetch pending event + * @client: client to operate on + * @eventp: output argument to store next event + * + * This fetches the next pending event from the event-queue and returns it to a + * caller. A pointer to the event is stored in @eventp. If there is no more + * event queued, NULL is returned. + * + * If a valid event is returned, it is accessible until the next call to this + * function, or the destruction of the context object (this might be either the + * client object or the probe object, pointed to by the event), whichever + * happens first. + * That is, the caller should not pin the returned event object, but copy + * required information into their own state tracking contexts. + * + * The possible events are: + * * N_DHCP4_CLIENT_EVENT_OFFER: A lease offered from a server in response + * to a probe. Several such offers may be + * received until one of them is selected by + * the caller. Only one lease may be selected. + * The attached lease object may be queried + * for information in order to decide which + * lease to select, though the information is + * not guaranteed to stay the same in the + * final lease. + * * N_DHCP4_CLIENT_EVENT_GRANTED: A selected lease was granted by the server. + * The information in the attached lease + * object should be used to configure the + * client. Once the client has been + * configured, the lease should be accepted. + * * N_DHCP4_CLIENT_EVENT_RETRACTED: A selected lease offer was retracted by the + * server. This can happen in case the server + * offers the same lease to several clients, + * or the server discovers that the IP address + * in the lease is already in use. + * * N_DHCP4_CLIENT_EVENT_EXTENDED: An active lease is extended, if applicable + * the kernel should be updated with the new + * lifetime information for addresses and/or + * routes. + * * N_DHCP4_CLIENT_EVENT_EXPIRED: An active lease failed to be extended by + * the end of its lifetime. The client should + * immediately stop using the information + * contained in the lease. + * * N_DHCP4_CLIENT_EVENT_DOWN: The network interface was put down down. + * The user is recommended to reestablish the + * lease at the first opportunity when the + * network comes back up. Note that this is + * purely informational, the probe will keep + * running, and if the network topology does + * not change any lease we have will still be + * valid. + * * N_DHCP4_CLIENT_EVENT_CANCELLED: The probe was cancelled. This can happen if + * the client attempted several incompatible + * probes in parallel, then the most recent + * ones will be cancelled asynchronously. + * + * Return: 0 on success, negative error code on failure. + */ +_c_public_ int n_dhcp4_client_pop_event(NDhcp4Client *client, NDhcp4ClientEvent **eventp) { + NDhcp4CEventNode *node, *t_node; + + c_list_for_each_entry_safe(node, t_node, &client->event_list, client_link) { + if (node->is_public) { + n_dhcp4_c_event_node_free(node); + continue; + } + + node->is_public = true; + *eventp = &node->event; + return 0; + } + + *eventp = NULL; + return 0; +} + +/** + * n_dhcp4_client_update_mtu() - update link mtu + * @client: client to operate on + * @mtu: new mtu + * + * This updates the link MTU used by the client object. By default, the minimum + * requirement given by the IP specification is assumed, which means 576 + * bytes. The caller is advised to update this to the actual MTU used by the + * link layer. + * + * This value reflects the MTU of the link layer. That is, it is the maximum + * packet size that you can send on that link, excluding the link-header but + * including the IP-header. On ethernet-v2 this would be 1500. + * + * If unsure, it is safe to leave this unset. However, in this case a DHCP + * server will be required to omit information if it does not fit into the + * default MTU. + * + * Unless you keep the default MTU, you should update the MTU whenever the link + * MTU changes. That is, when it is increased *and* when it is decreased. + * However, you must be aware that decreasing the MTU on a link might cause + * temporary data loss. + * + * Background: Knowing the link MTU guarantees that we can possibly transmit + * packets bigger than the IP minimum (i.e., 576 bytes). However, + * it does not guarantee that a possible target supports parsing + * packets bigger than the IP minimum. Hence, the MTU is used by a + * client to send a hint to a server that it can receive replies + * bigger than the minimum. As such, a server can reply with more + * information than otherwise possible. + * Since this DHCP client does not support fragmented packets, we + * simply set the allowed packet-size to the local link MTU. + * Note that DHCP relays might cause DHCP packets to be routed. + * However, such relays are required to always reassemble any + * fragments they receive into full DHCP packets, before they + * forward them either way. This guarantees that incoming packets + * are never fragmented, unless they exceed the local link MTU + * (this would otherwise not neccessarily be true, if some other + * part of the routed network had a lower MTU). + * + * Return: 0 on success, negative error code on failure. + */ +_c_public_ int n_dhcp4_client_update_mtu(NDhcp4Client *client, uint16_t mtu) { + int r; + + if (mtu == client->mtu) + return 0; + + if (client->current_probe) { + r = n_dhcp4_client_probe_update_mtu(client->current_probe, mtu); + if (r) + return r; + } + + client->mtu = mtu; + return 0; +} + +/** + * n_dhcp4_client_probe() - create a new probe + * @client: client to operate on + * @probep: output argument to store new probe + * @config: probe configuration to use + * + * This creates a new probe on @client. Probes represent DHCP requests and + * track the state over the entire lifetime of a lease. Once a probe is created + * it will start looking for DHCP servers, request a lease from them, and renew + * the lease continously whenever it expires. Furthermore, if a lease cannot be + * renewed, a new lease will be requested. + * + * The API allows for many probes to be run at the same time. However, the DHCP + * specification forbids many of those cases (e.g., you must not reuse a client + * id, otherwise it will be impossible to track who to forward received packets + * to). Hence, so far only a single probe can run at a time. If you create a + * new probe, all older probes that conflict with that probe will be canceled + * (their state machine is halted and a N_DHCP4_CLIENT_EVENT_CANCELLED event is + * raised. + * This might change in the future, though. There might be cases where multiple + * probes can be run in parallel (e.g., with different client-ids, or an INFORM + * in parallel to a REQUEST, ...). + * + * Return: 0 on success, negative error code on failure. + */ +_c_public_ int n_dhcp4_client_probe(NDhcp4Client *client, + NDhcp4ClientProbe **probep, + NDhcp4ClientProbeConfig *config) { + _c_cleanup_(n_dhcp4_client_probe_freep) NDhcp4ClientProbe *probe = NULL; + uint64_t ns_now; + int r; + + ns_now = n_dhcp4_gettime(CLOCK_BOOTTIME); + + r = n_dhcp4_client_probe_new(&probe, config, client, ns_now); + if (r) + return r; + + n_dhcp4_client_arm_timer(client); + + *probep = probe; + probe = NULL; + return 0; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-incoming.c b/shared/n-dhcp4/src/n-dhcp4-incoming.c new file mode 100644 index 000000000..255da4584 --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-incoming.c @@ -0,0 +1,431 @@ +/* + * DHCPv4 Incoming Messages + * + * This file implements the message parser object for incoming DHCP4 messages. + * It takes a linear data blob as input, and provides accessors for the message + * content. + * + * This wrapper mainly deals with the OPTIONs array. That is, in hides the + * different overload-sections the DHCP4 spec defines, it concatenates + * duplicate option fields (as described by the spec), and provides a + * consistent view to the caller. + * + * Internally, for every incoming message we linearize its OPTIONs. This means, + * we create a copy of the contents, and merge all duplicate options into a + * single option entry. We then provide accessors to the caller to easily get + * O(1) access to individual fields. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" + +static void n_dhcp4_incoming_prefetch(NDhcp4Incoming *incoming, size_t *offset, uint8_t option, const uint8_t *raw, size_t n_raw) { + uint8_t o, l; + size_t pos; + + for (pos = 0; pos < n_raw; ) { + o = raw[pos++]; + if (o == N_DHCP4_OPTION_PAD) + continue; + if (o == N_DHCP4_OPTION_END) + return; + + /* bail out if no remaining space for length field */ + if (pos >= n_raw) + return; + + /* bail out if length exceeds the available space */ + l = raw[pos++]; + if (l > n_raw || pos > n_raw - l) + return; + + /* prefetch content if it matches @option */ + if (o == option) { + memcpy((uint8_t *)&incoming->message + *offset, raw + pos, l); + *offset += l; + } + + pos += l; + } +} + +static void n_dhcp4_incoming_merge(NDhcp4Incoming *incoming, size_t *offset, uint8_t overload, uint8_t option) { + uint8_t *m = (uint8_t *)&incoming->message; + size_t pos; + + /* + * Prefetch all options matching @option from the 3 sections, + * concatenating their content. Remember the offset and size of the + * option in our message state. + */ + + pos = *offset; + + /* prefetch option from OPTIONS */ + n_dhcp4_incoming_prefetch(incoming, offset, option, + m + offsetof(NDhcp4Message, options), + incoming->n_message - offsetof(NDhcp4Message, options)); + + /* prefetch option from FILE */ + if (overload & N_DHCP4_OVERLOAD_FILE) + n_dhcp4_incoming_prefetch(incoming, offset, option, + m + offsetof(NDhcp4Message, file), + sizeof(incoming->message.file)); + + /* prefetch option from SNAME */ + if (overload & N_DHCP4_OVERLOAD_SNAME) + n_dhcp4_incoming_prefetch(incoming, offset, option, + m + offsetof(NDhcp4Message, sname), + sizeof(incoming->message.sname)); + + incoming->options[option].value = m + pos; + incoming->options[option].size = *offset - pos; +} + +static void n_dhcp4_incoming_linearize(NDhcp4Incoming *incoming) { + uint8_t *m, o, l, overload; + size_t i, pos, end, offset; + + /* + * Linearize all OPTIONs of the incoming message. We know that + * @incoming->message is preallocated to be big enough to hold the + * entire linearized message _trailing_ the original copy. All we have + * to do is walk the raw message in @incoming->message and for each + * option we find, copy it into the trailing space, concatenating all + * instances we find. + * + * Before we can copy the individual options, we must scan for the + * OVERLOAD option. This is required so our prefetcher knows which data + * arrays to scan for prefetching. + * + * So far, we require the OVERLOAD option to be present in the + * options-array (which is obvious and a given). However, if the option + * occurs multiple times outside of the options-array (i.e., SNAME or + * FILE), we silently ignore them. The specification does not allow + * multiple OVERLOAD options, anyway. Hence, this behavior only defines + * what we do when we see broken implementations, and we currently seem + * to support all styles we saw in the wild so far. + */ + + m = (uint8_t *)&incoming->message; + offset = incoming->n_message; + + n_dhcp4_incoming_merge(incoming, &offset, 0, N_DHCP4_OPTION_OVERLOAD); + if (incoming->options[N_DHCP4_OPTION_OVERLOAD].size >= 1) + overload = *incoming->options[N_DHCP4_OPTION_OVERLOAD].value; + else + overload = 0; + + for (i = 0; i < 3; ++i) { + if (i == 0) { /* walk OPTIONS */ + pos = offsetof(NDhcp4Message, options); + end = incoming->n_message; + } else if (i == 1) { /* walk FILE */ + if (!(overload & N_DHCP4_OVERLOAD_FILE)) + continue; + + pos = offsetof(NDhcp4Message, file); + end = pos + sizeof(incoming->message.file); + } else { /* walk SNAME */ + if (!(overload & N_DHCP4_OVERLOAD_SNAME)) + continue; + + pos = offsetof(NDhcp4Message, sname); + end = pos + sizeof(incoming->message.sname); + } + + while (pos < end) { + o = m[pos++]; + if (o == N_DHCP4_OPTION_PAD) + continue; + if (o == N_DHCP4_OPTION_END) + break; + if (pos >= end) + break; + + l = m[pos++]; + if (l > end || pos > end - l) + break; + + if (!incoming->options[o].value) + n_dhcp4_incoming_merge(incoming, &offset, overload, o); + + pos += l; + } + } +} + +/** + * n_dhcp4_incoming_new() - Allocate new incoming message object + * @incomingp: output argument for new object + * @raw: raw message blob + * @n_raw: length of the raw message blob + * + * This function allocates a new incoming-message object to wrap a received + * message blob. It performs basic verification of the message length and + * header, and then linearizes the DHCP4 options. + * + * The incoming-message object mainly provides accessors for the option-array. + * It handles all the different quirks around parsing and concatenating the + * options array, and provides the assembled data to the caller. It does not, + * however, in any way interpret the data of the individual options. This is up + * to the caller to do. + * + * Return: 0 on success, negative error code on failure, N_DHCP4_E_MALFORMED if + * the message is not a valid DHCP4 message. + */ +int n_dhcp4_incoming_new(NDhcp4Incoming **incomingp, const void *raw, size_t n_raw) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL; + size_t size; + + if (n_raw < sizeof(NDhcp4Message) || n_raw > UINT16_MAX) + return N_DHCP4_E_MALFORMED; + + /* + * Allocate enough space for book-keeping, a copy of @raw and trailing + * space for linearized options. The trailing space must be big enough + * to hold the entire options array unmodified (linearizing can only + * make it smaller). Hence, just allocate enough space to hold the raw + * message without the header. + */ + size = sizeof(*incoming) + n_raw - sizeof(NDhcp4Message); + size += n_raw - sizeof(NDhcp4Header); + + incoming = calloc(1, size); + if (!incoming) + return -ENOMEM; + + *incoming = (NDhcp4Incoming)N_DHCP4_INCOMING_NULL(*incoming); + incoming->n_message = n_raw; + memcpy(&incoming->message, raw, n_raw); + + if (incoming->message.magic != htobe32(N_DHCP4_MESSAGE_MAGIC)) + return N_DHCP4_E_MALFORMED; + + /* linearize options */ + n_dhcp4_incoming_linearize(incoming); + + *incomingp = incoming; + incoming = NULL; + return 0; +} + +/** + * n_dhcp4_incoming_free() - Deallocate message object + * @incoming: object to operate on, or NULL + * + * This deallocates and frees the given incoming-message object. If NULL is + * passed, this is a no-op. + * + * Return: NULL is returned. + */ +NDhcp4Incoming *n_dhcp4_incoming_free(NDhcp4Incoming *incoming) { + if (!incoming) + return NULL; + + free(incoming); + + return NULL; +} + +/** + * n_dhcp4_incoming_get_header() - Return message header + * @incoming: message to operate on + * + * This returns a pointer to the message header. Note that modifications to + * this header are permanent and will affect the original message. + * + * Return: A pointer to the message header is returned. + */ +NDhcp4Header *n_dhcp4_incoming_get_header(NDhcp4Incoming *incoming) { + return &incoming->message.header; +} + +/** + * n_dhcp4_incoming_get_raw() - Get access to the raw original message + * @incoming: message to operate on + * @rawp: output argument for the raw blob, or NULL + * + * This hands out a pointer to the raw message blob to the caller. This will + * point to the original message content, rather than the linearized version. + * + * Note that if the caller queried the contents of the message before, any + * modifications done by the caller will not affect the original message. It + * only affects the linearized content (which is a duplicate trailing the + * original message). However, modifications to the message header *DO* also + * appear in the original, since the message header is not duplicated. + * + * In either case, it is better to never modify the message, if you intend to + * forward it further. + * + * Return: The length of the raw message blob is returned. + */ +size_t n_dhcp4_incoming_get_raw(NDhcp4Incoming *incoming, const void **rawp) { + if (rawp) + *rawp = &incoming->message; + return incoming->n_message; +} + +/** + * n_dhcp4_incoming_query() - Query the contents of a specific option + * @incoming: message to query + * @option: option to look for + * @datap: output argument for the option-data, or NULL + * @n_datap: output argument for the length of the option, or NULL + * + * This returns a pointer to the requested option blob in the message. It + * points to a linearized version of all respective option-fields of the same + * type. Hence, the caller is not required to deal with multiple occurrences of + * the same option. + * + * If an option was not present in the incoming message, N_DHCP4_E_UNSET is + * returned. Note that this is different from an empty option! And empty option + * will return a valid pointer and size 0. + * + * Note that the pointer to the option-blob does not point to the original + * message, but a duplicated version. Modifications to the blob will not be + * reflected in the original message, but they will be permanent regarding + * further queries through this function. + * + * Note that the original message alignment might no longer be reflected in the + * returned blob. You must not alias the content of the blob, but always copy + * it out, or consume piecemeal. + * + * This function runs in O(1). + * + * Return: 0 on success, negative error code on failure, N_DHCP4_E_UNSET if the + * option was not found, + */ +int n_dhcp4_incoming_query(NDhcp4Incoming *incoming, uint8_t option, uint8_t **datap, size_t *n_datap) { + if (!incoming->options[option].value) + return N_DHCP4_E_UNSET; + + if (datap) + *datap = incoming->options[option].value; + if (n_datap) + *n_datap = incoming->options[option].size; + return 0; +} + +static int n_dhcp4_incoming_query_u8(NDhcp4Incoming *message, uint8_t option, uint8_t *u8p) { + uint8_t *data; + size_t n_data; + int r; + + r = n_dhcp4_incoming_query(message, option, &data, &n_data); + if (r) + return r; + else if (n_data != sizeof(*data)) + return N_DHCP4_E_MALFORMED; + + *u8p = *data; + return 0; +} + +static int n_dhcp4_incoming_query_u16(NDhcp4Incoming *message, uint8_t option, uint16_t *u16p) { + uint8_t *data; + size_t n_data; + uint16_t be16; + int r; + + r = n_dhcp4_incoming_query(message, option, &data, &n_data); + if (r) + return r; + else if (n_data != sizeof(be16)) + return N_DHCP4_E_MALFORMED; + + memcpy(&be16, data, sizeof(be16)); + + *u16p = ntohs(be16); + return 0; +} + +static int n_dhcp4_incoming_query_u32(NDhcp4Incoming *message, uint8_t option, uint32_t *u32p) { + uint8_t *data; + size_t n_data; + uint32_t be32; + int r; + + r = n_dhcp4_incoming_query(message, option, &data, &n_data); + if (r) + return r; + else if (n_data != sizeof(be32)) + return N_DHCP4_E_MALFORMED; + + memcpy(&be32, data, sizeof(be32)); + + if (be32 == (uint32_t)-1) + *u32p = 0; + else + *u32p = ntohl(be32); + return 0; +} + +static int n_dhcp4_incoming_query_in_addr(NDhcp4Incoming *message, uint8_t option, struct in_addr *addrp) { + uint8_t *data; + size_t n_data; + uint32_t be32; + int r; + + r = n_dhcp4_incoming_query(message, option, &data, &n_data); + if (r) + return r; + else if (n_data != sizeof(be32)) + return N_DHCP4_E_MALFORMED; + + memcpy(&be32, data, sizeof(be32)); + + addrp->s_addr = be32; + return 0; +} + +int n_dhcp4_incoming_query_message_type(NDhcp4Incoming *message, uint8_t *typep) { + return n_dhcp4_incoming_query_u8(message, N_DHCP4_OPTION_MESSAGE_TYPE, typep); +} + +int n_dhcp4_incoming_query_lifetime(NDhcp4Incoming *message, uint32_t *lifetimep) { + return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, lifetimep); +} + +int n_dhcp4_incoming_query_t2(NDhcp4Incoming *message, uint32_t *t2p) { + return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_REBINDING_T2_TIME, t2p); +} + +int n_dhcp4_incoming_query_t1(NDhcp4Incoming *message, uint32_t *t1p) { + return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_RENEWAL_T1_TIME, t1p); +} + +int n_dhcp4_incoming_query_server_identifier(NDhcp4Incoming *message, struct in_addr *idp) { + return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, idp); +} + +int n_dhcp4_incoming_query_max_message_size(NDhcp4Incoming *message, uint16_t *max_message_sizep) { + return n_dhcp4_incoming_query_u16(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, max_message_sizep); +} + +int n_dhcp4_incoming_query_requested_ip(NDhcp4Incoming *message, struct in_addr *requested_ipp) { + return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, requested_ipp); +} + +void n_dhcp4_incoming_get_xid(NDhcp4Incoming *message, uint32_t *xidp) { + NDhcp4Header *header = n_dhcp4_incoming_get_header(message); + + *xidp = header->xid; +} + +void n_dhcp4_incoming_get_yiaddr(NDhcp4Incoming *message, struct in_addr *yiaddr) { + NDhcp4Header *header = n_dhcp4_incoming_get_header(message); + + yiaddr->s_addr = header->yiaddr; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-outgoing.c b/shared/n-dhcp4/src/n-dhcp4-outgoing.c new file mode 100644 index 000000000..bb33bbdbe --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-outgoing.c @@ -0,0 +1,371 @@ +/* + * DHCPv4 Outgoing Messages + * + * XXX + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" + +/** + * N_DHCP4_OUTGOING_MAX_PHDR - maximum protocol header size + * + * All DHCP4 messages-limits specify the size of the entire packet including + * the protocol layer (i.e., including the IP headers and UDP headers). To + * calculate the size we have remaining for the actual DHCP message, we need to + * substract the maximum possible header-length the linux-kernel might prepend + * to our messages. This turns out to be the maximum IP-header size (including + * optional IP headers, hence 60 bytes) plus the UDP header size (i.e., 8 + * bytes). + */ +#define N_DHCP4_OUTGOING_MAX_PHDR (N_DHCP4_NETWORK_IP_MAXIMUM_HEADER_SIZE + sizeof(struct udphdr)) + +/** + * n_dhcp4_outgoing_new() - Allocate new outgoing message + * @outgoingp: output argument to return allocate object through + * @max_size: maximum transmission size to use + * @overload: select sections to overload + * + * This allocates a new outgoing message and returns it to the caller. The + * caller can then append data to it and send it over the wire. + * + * The @max_size parameter specifies the transport-layer MTU to consider. If 0, + * N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE is used. Note that this argument + * specifies the maximum packet size *INCLUDING* the IP-headers and UDP-header. + * Internally, the allocator makes sure to never create packets bigger than the + * specified MTU. The append functions will return an error, if the packet size + * would exceed the MTU. + * If you use a full UDP stack that supports packet fragmentation, you can + * specify the maximum packet size here (e.g., UINT16_MAX). + * + * Return: 0 on success, error code on failure. + */ +int n_dhcp4_outgoing_new(NDhcp4Outgoing **outgoingp, size_t max_size, uint8_t overload) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL; + + c_assert(!(overload & ~(N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME))); + + /* + * Make sure the minimum limit is bigger than the maximum protocol + * header plus the DHCP-message-header plus a single OPTION_END byte. + */ + static_assert(N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE >= N_DHCP4_OUTGOING_MAX_PHDR + + sizeof(NDhcp4Message) + 1, + "Invalid minimum IP packet limit"); + + outgoing = calloc(1, sizeof(*outgoing)); + if (!outgoing) + return -ENOMEM; + + *outgoing = (NDhcp4Outgoing)N_DHCP4_OUTGOING_NULL(*outgoing); + outgoing->n_message = N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE - N_DHCP4_OUTGOING_MAX_PHDR; + outgoing->i_message = offsetof(NDhcp4Message, options); + outgoing->max_size = outgoing->n_message; + outgoing->overload = overload; + + if (max_size > N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE) + outgoing->max_size = max_size - N_DHCP4_OUTGOING_MAX_PHDR; + + outgoing->message = calloc(1, outgoing->n_message); + if (!outgoing->message) + return -ENOMEM; + + outgoing->message->magic = htonl(N_DHCP4_MESSAGE_MAGIC); + outgoing->message->options[0] = N_DHCP4_OPTION_END; + + *outgoingp = outgoing; + outgoing = NULL; + return 0; +} + +/** + * n_dhcp4_outgoing_free() - Deallocate outgoing message + * @outgoing: message to deallocate, or NULL + * + * This is the opposite to n_dhcp4_outgoing_new(). It deallocates and frees the + * passed object. If @outgoing is NULL, this is a no-op. + * + * Return: NULL is returned. + */ +NDhcp4Outgoing *n_dhcp4_outgoing_free(NDhcp4Outgoing *outgoing) { + if (!outgoing) + return NULL; + + free(outgoing->message); + free(outgoing); + + return NULL; +} + +/** + * n_dhcp4_outgoing_get_header() - Get pointer to the message header + * @outgoing: message to operate on + * + * This returns a pointer to the DHCP4 message header to the caller. The caller + * can use this to fill-in the header-fields. Note that all fields are + * initialized to their default values. Hence, you only need to override the + * fields where the default is not sufficient. + * + * Return: A pointer to the message header is returned. + */ +NDhcp4Header *n_dhcp4_outgoing_get_header(NDhcp4Outgoing *outgoing) { + return &outgoing->message->header; +} + +/** + * n_dhcp4_outgoing_get_raw() - Get the raw message blob + * @outgoing: message to operat on + * @rawp: output argument for the message-blob + * + * This function gives the caller access to the raw message-blob. That is, once + * message-marshaling is complete, use this to get the raw blob for sending. + * Note that this blob is only valid as long as you no longer append any + * further options to the message, nor modify it in any other way. + * + * Return: The size of the raw message blob is returned. + */ +size_t n_dhcp4_outgoing_get_raw(NDhcp4Outgoing *outgoing, const void **rawp) { + if (rawp) + *rawp = outgoing->message; + + /* + * Return the DHCP message until the END option, excluding any + * trailing padding. We overallocate during append, so the + * allocated message might be bigger than what we want to + * send on the wire. + */ + return outgoing->i_message + 1; +} + +static void n_dhcp4_outgoing_append_option(NDhcp4Outgoing *outgoing, + uint8_t option, + const void *data, + uint8_t n_data) { + uint8_t *blob = (void *)outgoing->message; + + blob[outgoing->i_message++] = option; + blob[outgoing->i_message++] = n_data; + memcpy(blob + outgoing->i_message, data, n_data); + outgoing->i_message += n_data; + blob[outgoing->i_message] = N_DHCP4_OPTION_END; +} + +/** + * n_dhcp4_outgoing_append() - Append option to outgoing message + * @outgoing: message to operate on + * @option: option code to append + * @data: data to append in the option + * @n_data: length of the data blob + * + * This appends another option to the given outgoing message. The data is taken + * verbatim and copied into the message. Note that no validation is done. If + * you provide an option multiple times, it will be added multiple times (spec + * then requires them to be interpreted as concatenated option, in case the + * option is marked as such). + * + * The size of a message is limited, based on the restriction passed to the + * outgoing-message constructor. If there is not enough free space to copy in + * the new option, N_DHCP4_E_NO_SPACE is returned. + * + * The order in which you append options might matter to some implementations. + * For example, the message-type is often expected to be the first option. We + * do not place such restrictions, but for compatibility with external + * implementations, you should follow these recommendations. + * Furthermore, we do not implement any kind of smart allocators. That is, all + * options are simply appended when you call this. But due to the overloading + * feature, fragmentation might matter. Hence, if you use overloading, overly + * big options might cause padding, and as such waste space. + * + * Return: 0 on success, negative error code on failure, N_DHCP4_E_NO_SPACE + * when there is not sufficient free space in the message. + */ +int n_dhcp4_outgoing_append(NDhcp4Outgoing *outgoing, + uint8_t option, + const void *data, + uint8_t n_data) { + NDhcp4Message *m; + uint8_t overload; + size_t rem, n; + + c_assert(option != N_DHCP4_OPTION_PAD); + c_assert(option != N_DHCP4_OPTION_END); + c_assert(option != N_DHCP4_OPTION_OVERLOAD); + + /* + * If the iterator is on the OPTIONs field, try appending the new blob. + * We need 2 header-bytes plus @n_data bytes. Additionally, we always + * reserve 3 trailing bytes for a possible OVERLOAD option, and 1 byte + * for the END marker. + */ + if (outgoing->i_message >= offsetof(NDhcp4Message, options)) { + rem = outgoing->n_message - outgoing->i_message; + + /* try fitting into remaining OPTIONs space */ + if (rem >= n_data + 2U + 3U + 1U) { + n_dhcp4_outgoing_append_option(outgoing, option, data, n_data); + return 0; + } + + /* try fitting into allowed OPTIONs space */ + if (outgoing->max_size - outgoing->i_message >= n_data + 2U + 3U + 1U) { + /* try over-allocation to reduce allocation pressure */ + n = c_min(outgoing->max_size, + outgoing->n_message + n_data + 128); + m = realloc(outgoing->message, n); + if (!m) + return -ENOMEM; + + memset((void *)m + outgoing->i_message, 0, n - outgoing->i_message); + outgoing->message = m; + outgoing->n_message = n; + n_dhcp4_outgoing_append_option(outgoing, option, data, n_data); + return 0; + } + + /* not enough remaining space, try OVERLOAD */ + if (!outgoing->overload) + return N_DHCP4_E_NO_SPACE; + + /* + * We ran out of space in the OPTIONs array, but overloading + * was enabled. This means, we can insert an OVERLOAD option + * and then use SNAME/FILE to store more options. + * Note that the three different sections cannot overlap and + * all must have an END marker. So as soon as we add the + * OVERLOAD option, we must make sure the other sections have + * the valid END marker. From then on, our *_append_option() + * helper makes sure to move the END marker with every + * insertion. + */ + overload = outgoing->overload; + n_dhcp4_outgoing_append_option(outgoing, N_DHCP4_OPTION_OVERLOAD, &overload, 1); + + if (overload & N_DHCP4_OVERLOAD_FILE) + outgoing->message->file[0] = N_DHCP4_OPTION_END; + if (overload & N_DHCP4_OVERLOAD_SNAME) + outgoing->message->sname[0] = N_DHCP4_OPTION_END; + + if (overload & N_DHCP4_OVERLOAD_FILE) + outgoing->i_message = offsetof(NDhcp4Message, file); + else if (overload & N_DHCP4_OVERLOAD_SNAME) + outgoing->i_message = offsetof(NDhcp4Message, sname); + } + + /* + * The OPTIONs section is full and OVERLOAD was enabled. Try writing + * into the FILE section. Always reserve 1 byte for the trailing END + * marker. + */ + if (outgoing->i_message >= offsetof(NDhcp4Message, file)) { + rem = sizeof(outgoing->message->file); + rem -= outgoing->i_message - offsetof(NDhcp4Message, file); + + if (rem >= n_data + 2U + 1U) { + n_dhcp4_outgoing_append_option(outgoing, option, data, n_data); + return 0; + } + + if (overload & N_DHCP4_OVERLOAD_SNAME) + outgoing->i_message = offsetof(NDhcp4Message, sname); + else + return N_DHCP4_E_NO_SPACE; + } + + /* + * OPTIONs and FILE are full, try putting data into the SNAME section + * as a last resort. + */ + if (outgoing->i_message >= offsetof(NDhcp4Message, sname)) { + rem = sizeof(outgoing->message->sname); + rem -= outgoing->i_message - offsetof(NDhcp4Message, sname); + + if (rem >= n_data + 2U + 1U) { + n_dhcp4_outgoing_append_option(outgoing, option, data, n_data); + return 0; + } + } + + return N_DHCP4_E_NO_SPACE; +} + +static int n_dhcp4_outgoing_append_u32(NDhcp4Outgoing *message, uint8_t option, uint32_t u32) { + uint32_t be32 = htonl(u32); + int r; + + r = n_dhcp4_outgoing_append(message, option, &be32, sizeof(be32)); + if (r) + return r; + + return 0; +} + +static int n_dhcp4_outgoing_append_in_addr(NDhcp4Outgoing *message, uint8_t option, struct in_addr addr) { + int r; + + r = n_dhcp4_outgoing_append(message, option, &addr.s_addr, sizeof(addr.s_addr)); + if (r) + return r; + + return 0; +} + +int n_dhcp4_outgoing_append_t1(NDhcp4Outgoing *message, uint32_t t1) { + return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_RENEWAL_T1_TIME, t1); +} + +int n_dhcp4_outgoing_append_t2(NDhcp4Outgoing *message, uint32_t t2) { + return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_REBINDING_T2_TIME, t2); +} + +int n_dhcp4_outgoing_append_lifetime(NDhcp4Outgoing *message, uint32_t lifetime) { + return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, lifetime); +} + +int n_dhcp4_outgoing_append_server_identifier(NDhcp4Outgoing *message, struct in_addr addr) { + return n_dhcp4_outgoing_append_in_addr(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, addr); +} + +int n_dhcp4_outgoing_append_requested_ip(NDhcp4Outgoing *message, struct in_addr addr) { + return n_dhcp4_outgoing_append_in_addr(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, addr); +} + +void n_dhcp4_outgoing_set_secs(NDhcp4Outgoing *message, uint32_t secs) { + NDhcp4Header *header = n_dhcp4_outgoing_get_header(message); + + /* + * Some DHCP servers will reject DISCOVER or REQUEST messages if 'secs' + * is not set (i.e., set to 0), even though the spec allows it. + */ + c_assert(secs); + + header->secs = htonl(secs); +} + +void n_dhcp4_outgoing_set_xid(NDhcp4Outgoing *message, uint32_t xid) { + NDhcp4Header *header = n_dhcp4_outgoing_get_header(message); + + header->xid = xid; +} + +void n_dhcp4_outgoing_get_xid(NDhcp4Outgoing *message, uint32_t *xidp) { + NDhcp4Header *header = n_dhcp4_outgoing_get_header(message); + + *xidp = header->xid; +} + +void n_dhcp4_outgoing_set_yiaddr(NDhcp4Outgoing *message, struct in_addr yiaddr) { + NDhcp4Header *header = n_dhcp4_outgoing_get_header(message); + + header->yiaddr = yiaddr.s_addr; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-private.h b/shared/n-dhcp4/src/n-dhcp4-private.h new file mode 100644 index 000000000..b896a93dc --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-private.h @@ -0,0 +1,689 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" + +typedef struct NDhcp4CConnection NDhcp4CConnection; +typedef struct NDhcp4CEventNode NDhcp4CEventNode; +typedef struct NDhcp4ClientProbeOption NDhcp4ClientProbeOption; +typedef struct NDhcp4Header NDhcp4Header; +typedef struct NDhcp4Incoming NDhcp4Incoming; +typedef struct NDhcp4Message NDhcp4Message; +typedef struct NDhcp4Outgoing NDhcp4Outgoing; +typedef struct NDhcp4SConnection NDhcp4SConnection; +typedef struct NDhcp4SConnectionIp NDhcp4SConnectionIp; +typedef struct NDhcp4SEventNode NDhcp4SEventNode; + +/* specs */ + +#define N_DHCP4_NETWORK_IP_MAXIMUM_HEADER_SIZE (60) /* See RFC791 */ +#define N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE (576) /* See RFC791 */ +#define N_DHCP4_NETWORK_SERVER_PORT (67) +#define N_DHCP4_NETWORK_CLIENT_PORT (68) +#define N_DHCP4_MESSAGE_MAGIC ((uint32_t)(0x63825363)) +#define N_DHCP4_MESSAGE_FLAG_BROADCAST (htons(0x8000)) + +enum { + N_DHCP4_OP_BOOTREQUEST = 1, + N_DHCP4_OP_BOOTREPLY = 2, +}; + +enum { + N_DHCP4_OPTION_PAD = 0, + N_DHCP4_OPTION_SUBNET_MASK = 1, + N_DHCP4_OPTION_TIME_OFFSET = 2, + N_DHCP4_OPTION_ROUTER = 3, + N_DHCP4_OPTION_DOMAIN_NAME_SERVER = 6, + N_DHCP4_OPTION_HOST_NAME = 12, + N_DHCP4_OPTION_BOOT_FILE_SIZE = 13, + N_DHCP4_OPTION_DOMAIN_NAME = 15, + N_DHCP4_OPTION_ROOT_PATH = 17, + N_DHCP4_OPTION_ENABLE_IP_FORWARDING = 19, + N_DHCP4_OPTION_ENABLE_IP_FORWARDING_NL = 20, + N_DHCP4_OPTION_POLICY_FILTER = 21, + N_DHCP4_OPTION_INTERFACE_MDR = 22, + N_DHCP4_OPTION_INTERFACE_TTL = 23, + N_DHCP4_OPTION_INTERFACE_MTU_AGING_TIMEOUT = 24, + N_DHCP4_OPTION_INTERFACE_MTU = 26, + N_DHCP4_OPTION_BROADCAST = 28, + N_DHCP4_OPTION_STATIC_ROUTE = 33, + N_DHCP4_OPTION_NTP_SERVER = 42, + N_DHCP4_OPTION_VENDOR_SPECIFIC = 43, + N_DHCP4_OPTION_REQUESTED_IP_ADDRESS = 50, + N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME = 51, + N_DHCP4_OPTION_OVERLOAD = 52, + N_DHCP4_OPTION_MESSAGE_TYPE = 53, + N_DHCP4_OPTION_SERVER_IDENTIFIER = 54, + N_DHCP4_OPTION_PARAMETER_REQUEST_LIST = 55, + N_DHCP4_OPTION_ERROR_MESSAGE = 56, + N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE = 57, + N_DHCP4_OPTION_RENEWAL_T1_TIME = 58, + N_DHCP4_OPTION_REBINDING_T2_TIME = 59, + N_DHCP4_OPTION_VENDOR_CLASS_IDENTIFIER = 60, + N_DHCP4_OPTION_CLIENT_IDENTIFIER = 61, + N_DHCP4_OPTION_FQDN = 81, + N_DHCP4_OPTION_NEW_POSIX_TIMEZONE = 100, + N_DHCP4_OPTION_NEW_TZDB_TIMEZONE = 101, + N_DHCP4_OPTION_CLASSLESS_STATIC_ROUTE = 121, + N_DHCP4_OPTION_PRIVATE_BASE = 224, + N_DHCP4_OPTION_PRIVATE_LAST = 254, + N_DHCP4_OPTION_END = 255, + _N_DHCP4_OPTION_N = 256, +}; + +enum { + N_DHCP4_OVERLOAD_FILE = 1, + N_DHCP4_OVERLOAD_SNAME = 2, +}; + +enum { + N_DHCP4_MESSAGE_DISCOVER = 1, + N_DHCP4_MESSAGE_OFFER = 2, + N_DHCP4_MESSAGE_REQUEST = 3, + N_DHCP4_MESSAGE_DECLINE = 4, + N_DHCP4_MESSAGE_ACK = 5, + N_DHCP4_MESSAGE_NAK = 6, + N_DHCP4_MESSAGE_RELEASE = 7, + N_DHCP4_MESSAGE_INFORM = 8, + N_DHCP4_MESSAGE_FORCERENEW = 9, +}; + +struct NDhcp4Header { + uint8_t op; + uint8_t htype; + uint8_t hlen; + uint8_t hops; + uint32_t xid; + uint16_t secs; + uint16_t flags; + uint32_t ciaddr; + uint32_t yiaddr; + uint32_t siaddr; + uint32_t giaddr; + uint8_t chaddr[16]; +} _c_packed_; + +struct NDhcp4Message { + NDhcp4Header header; + uint8_t sname[64]; + uint8_t file[128]; + uint32_t magic; + uint8_t options[]; +} _c_packed_; + +/* objects */ + +enum { + _N_DHCP4_E_INTERNAL = _N_DHCP4_E_N, + + N_DHCP4_E_UNEXPECTED, + + N_DHCP4_E_NO_SPACE, + N_DHCP4_E_MALFORMED, + + N_DHCP4_E_DROPPED, + N_DHCP4_E_DOWN, + N_DHCP4_E_AGAIN, +}; + +enum { + N_DHCP4_C_CONNECTION_STATE_INIT, + N_DHCP4_C_CONNECTION_STATE_PACKET, + N_DHCP4_C_CONNECTION_STATE_DRAINING, + N_DHCP4_C_CONNECTION_STATE_UDP, + N_DHCP4_C_CONNECTION_STATE_CLOSED, +}; + +enum { + N_DHCP4_CLIENT_EPOLL_TIMER, + N_DHCP4_CLIENT_EPOLL_IO, +}; + +enum { + N_DHCP4_CLIENT_PROBE_STATE_INIT, + N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT, + N_DHCP4_CLIENT_PROBE_STATE_SELECTING, + N_DHCP4_CLIENT_PROBE_STATE_REBOOTING, + N_DHCP4_CLIENT_PROBE_STATE_REQUESTING, + N_DHCP4_CLIENT_PROBE_STATE_GRANTED, + N_DHCP4_CLIENT_PROBE_STATE_BOUND, + N_DHCP4_CLIENT_PROBE_STATE_RENEWING, + N_DHCP4_CLIENT_PROBE_STATE_REBINDING, + N_DHCP4_CLIENT_PROBE_STATE_EXPIRED, +}; + +enum { + N_DHCP4_CLIENT_LEASE_STATE_INIT, + N_DHCP4_CLIENT_LEASE_STATE_OFFERED, + N_DHCP4_CLIENT_LEASE_STATE_SELECTED, + N_DHCP4_CLIENT_LEASE_STATE_DECLINED, + N_DHCP4_CLIENT_LEASE_STATE_ACKED, +}; + +enum { + N_DHCP4_SERVER_EPOLL_TIMER, + N_DHCP4_SERVER_EPOLL_IO, +}; + +enum { + _N_DHCP4_C_MESSAGE_INVALID = 0, + N_DHCP4_C_MESSAGE_DISCOVER, + N_DHCP4_C_MESSAGE_INFORM, + N_DHCP4_C_MESSAGE_SELECT, + N_DHCP4_C_MESSAGE_IGNORE, + N_DHCP4_C_MESSAGE_RENEW, + N_DHCP4_C_MESSAGE_REBIND, + N_DHCP4_C_MESSAGE_REBOOT, + N_DHCP4_C_MESSAGE_RELEASE, + N_DHCP4_C_MESSAGE_DECLINE, +}; + +struct NDhcp4Outgoing { + NDhcp4Message *message; + size_t n_message; + size_t i_message; + size_t max_size; + + uint8_t overload : 2; + + struct { + uint8_t type; + uint64_t start_time; + uint64_t base_time; + uint64_t send_time; + uint64_t send_jitter; + size_t n_send; + } userdata; +}; + +#define N_DHCP4_OUTGOING_NULL(_x) { \ + } + +struct NDhcp4Incoming { + struct { + uint8_t *value; + size_t size; + } options[_N_DHCP4_OPTION_N]; + + struct { + uint8_t type; + uint64_t start_time; + uint64_t base_time; + } userdata; + + size_t n_message; + NDhcp4Message message; + /* @message must be the last member */ +}; + +#define N_DHCP4_INCOMING_NULL(_x) { \ + } + +struct NDhcp4ClientConfig { + int ifindex; + unsigned int transport; + bool request_broadcast; + uint8_t mac[MAX_ADDR_LEN]; + size_t n_mac; + uint8_t broadcast_mac[MAX_ADDR_LEN]; + size_t n_broadcast_mac; + uint8_t *client_id; + size_t n_client_id; +}; + +#define N_DHCP4_CLIENT_CONFIG_NULL(_x) { \ + .transport = _N_DHCP4_TRANSPORT_N, \ + } + +struct NDhcp4ClientProbeOption { + uint8_t option; + uint8_t n_data; + uint8_t data[]; +}; + +#define N_DHCP4_CLIENT_PROBE_OPTION_NULL(_x) { \ + .option = N_DHCP4_OPTION_PAD, \ + } + +struct NDhcp4ClientProbeConfig { + bool inform_only; + bool init_reboot; + struct in_addr requested_ip; + struct drand48_data entropy; /* entropy pool */ + uint64_t ms_start_delay; /* max ms to wait before starting probe */ + NDhcp4ClientProbeOption *options[UINT8_MAX + 1]; + int8_t request_parameters[UINT8_MAX + 1]; + size_t n_request_parameters; +}; + +#define N_DHCP4_CLIENT_PROBE_CONFIG_NULL(_x) { \ + .ms_start_delay = N_DHCP4_CLIENT_START_DELAY_RFC2131, \ + } + +struct NDhcp4CEventNode { + CList client_link; + CList probe_link; + NDhcp4ClientEvent event; + bool is_public : 1; +}; + +#define N_DHCP4_C_EVENT_NODE_NULL(_x) { \ + .client_link = C_LIST_INIT((_x).client_link), \ + .probe_link = C_LIST_INIT((_x).probe_link), \ + } + +struct NDhcp4CConnection { + NDhcp4ClientConfig *client_config; + NDhcp4ClientProbeConfig *probe_config; + int fd_epoll; + + unsigned int state; /* current connection state */ + int fd_packet; /* packet socket */ + int fd_udp; /* udp socket */ + + NDhcp4Outgoing *request; /* current request */ + + uint32_t client_ip; /* client IP address, or 0 */ + uint32_t server_ip; /* server IP address, or 0 */ + uint16_t mtu; /* client mtu, or 0 */ + + /* + * When we get DHCP packets from the kernel, we need a buffer to read + * the data into. Since UDP packets can be up to 2^16 bytes in size, we + * avoid placing it on the stack and instead read into this scratch + * buffer. It is purely meant as stack replacement, no data is returned + * through this buffer. + */ + uint8_t scratch_buffer[UINT16_MAX]; +}; + +#define N_DHCP4_C_CONNECTION_NULL(_x) { \ + .fd_packet = -1, \ + .fd_udp = -1, \ + } + +struct NDhcp4Client { + unsigned long n_refs; + NDhcp4ClientConfig *config; + CList event_list; + int fd_epoll; + int fd_timer; + + uint16_t mtu; + NDhcp4ClientProbe *current_probe; + uint64_t scheduled_timeout; + + bool preempted : 1; +}; + +#define N_DHCP4_CLIENT_NULL(_x) { \ + .n_refs = 1, \ + .event_list = C_LIST_INIT((_x).event_list), \ + .fd_epoll = -1, \ + .fd_timer = -1, \ + } + +struct NDhcp4ClientProbe { + NDhcp4ClientProbeConfig *config; + NDhcp4Client *client; + CList event_list; + CList lease_list; + void *userdata; + + unsigned int state; /* current probe state */ + uint64_t ns_deferred; /* timeout for deferred action */ + NDhcp4ClientLease *current_lease; /* current lease */ + + NDhcp4CConnection connection; /* client connection wrapper */ +}; + +#define N_DHCP4_CLIENT_PROBE_NULL(_x) { \ + .event_list = C_LIST_INIT((_x).event_list), \ + .lease_list = C_LIST_INIT((_x).lease_list), \ + .connection = N_DHCP4_C_CONNECTION_NULL((_x).connection), \ + } + +struct NDhcp4ClientLease { + unsigned long n_refs; + + NDhcp4ClientProbe *probe; + CList probe_link; + + NDhcp4Incoming *message; + + uint64_t t1; + uint64_t t2; + uint64_t lifetime; +}; + +#define N_DHCP4_CLIENT_LEASE_NULL(_x) { \ + .n_refs = 1, \ + .probe_link = C_LIST_INIT((_x).probe_link), \ + } + +struct NDhcp4ServerConfig { + int ifindex; +}; + +#define N_DHCP4_SERVER_CONFIG_NULL(_x) { \ + } + +struct NDhcp4SEventNode { + CList server_link; + NDhcp4ServerEvent event; + bool is_public : 1; +}; + +#define N_DHCP4_S_EVENT_NODE_NULL(_x) { \ + .server_link = C_LIST_INIT((_x).server_link), \ + } + +struct NDhcp4SConnection { + int ifindex; /* interface index */ + int fd_packet; /* packet socket */ + int fd_udp; /* udp socket */ + uint8_t buf[UINT16_MAX]; /* scratch recevie buffer */ + + /* XXX: support a set of server addresses */ + NDhcp4SConnectionIp *ip; /* server IP address, or NULL */ +}; + +#define N_DHCP4_S_CONNECTION_NULL(_x) { \ + .fd_packet = -1, \ + .fd_udp = -1, \ + } + +struct NDhcp4SConnectionIp { + NDhcp4SConnection *connection; + struct in_addr ip; +}; + +#define N_DHCP4_S_CONNECTION_IP_NULL(_x) { \ +} + +struct NDhcp4Server { + unsigned long n_refs; + CList event_list; + CList lease_list; + + bool preempted : 1; + + NDhcp4SConnection connection; +}; + +#define N_DHCP4_SERVER_NULL(_x) { \ + .n_refs = 1, \ + .event_list = C_LIST_INIT((_x).event_list), \ + .lease_list = C_LIST_INIT((_x).lease_list), \ + .connection = N_DHCP4_S_CONNECTION_NULL((_x).connection), \ + } + +struct NDhcp4ServerIp { + NDhcp4SConnectionIp ip; +}; + +#define N_DHCP4_SERVER_IP_NULL(_x) { \ + .ip = N_DHCP4_S_CONNECTION_IP_NULL((_x).ip), \ + } + +struct NDhcp4ServerLease { + unsigned long n_refs; + + NDhcp4Server *server; + CList server_link; + + NDhcp4Incoming *request; + NDhcp4Incoming *reply; +}; + +#define N_DHCP4_SERVER_LEASE_NULL(_x) { \ + .n_refs = 1, \ + .server_link = C_LIST_INIT((_x).server_link), \ + } + +/* outgoing messages */ + +int n_dhcp4_outgoing_new(NDhcp4Outgoing **outgoingp, size_t max_size, uint8_t overload); +NDhcp4Outgoing *n_dhcp4_outgoing_free(NDhcp4Outgoing *outgoing); + +NDhcp4Header *n_dhcp4_outgoing_get_header(NDhcp4Outgoing *outgoing); +size_t n_dhcp4_outgoing_get_raw(NDhcp4Outgoing *outgoing, const void **rawp); +int n_dhcp4_outgoing_append(NDhcp4Outgoing *outgoing, uint8_t option, const void *data, uint8_t n_data); + +int n_dhcp4_outgoing_append_t1(NDhcp4Outgoing *message, uint32_t t1); +int n_dhcp4_outgoing_append_t2(NDhcp4Outgoing *message, uint32_t t2); +int n_dhcp4_outgoing_append_lifetime(NDhcp4Outgoing *message, uint32_t lifetime); +int n_dhcp4_outgoing_append_server_identifier(NDhcp4Outgoing *message, struct in_addr addr); +int n_dhcp4_outgoing_append_requested_ip(NDhcp4Outgoing *message, struct in_addr addr); + +void n_dhcp4_outgoing_set_secs(NDhcp4Outgoing *message, uint32_t secs); +void n_dhcp4_outgoing_set_xid(NDhcp4Outgoing *message, uint32_t xid); +void n_dhcp4_outgoing_set_yiaddr(NDhcp4Outgoing *message, struct in_addr yiaddr); + +void n_dhcp4_outgoing_get_xid(NDhcp4Outgoing *message, uint32_t *xidp); + +/* incoming messages */ + +int n_dhcp4_incoming_new(NDhcp4Incoming **incomingp, const void *raw, size_t n_raw); +NDhcp4Incoming *n_dhcp4_incoming_free(NDhcp4Incoming *incoming); + +NDhcp4Header *n_dhcp4_incoming_get_header(NDhcp4Incoming *incoming); +size_t n_dhcp4_incoming_get_raw(NDhcp4Incoming *incoming, const void **rawp); +int n_dhcp4_incoming_query(NDhcp4Incoming *incoming, uint8_t option, uint8_t **datap, size_t *n_datap); + +int n_dhcp4_incoming_query_message_type(NDhcp4Incoming *message, uint8_t *typep); +int n_dhcp4_incoming_query_lifetime(NDhcp4Incoming *message, uint32_t *lifetimep); +int n_dhcp4_incoming_query_t2(NDhcp4Incoming *message, uint32_t *t2p); +int n_dhcp4_incoming_query_t1(NDhcp4Incoming *message, uint32_t *t1p); +int n_dhcp4_incoming_query_server_identifier(NDhcp4Incoming *message, struct in_addr *idp); +int n_dhcp4_incoming_query_max_message_size(NDhcp4Incoming *message, uint16_t *max_message_sizep); +int n_dhcp4_incoming_query_requested_ip(NDhcp4Incoming *message, struct in_addr *requested_ipp); + +void n_dhcp4_incoming_get_xid(NDhcp4Incoming *message, uint32_t *xidp); +void n_dhcp4_incoming_get_yiaddr(NDhcp4Incoming *message, struct in_addr *yiaddr); + +/* sockets */ + +int n_dhcp4_c_socket_packet_new(int *sockfdp, int ifindex); +int n_dhcp4_c_socket_udp_new(int *sockfdp, + int ifindex, + const struct in_addr *client_addr, + const struct in_addr *server_addr); +int n_dhcp4_s_socket_packet_new(int *sockfdp); +int n_dhcp4_s_socket_udp_new(int *sockfdp, int ifindex); + +int n_dhcp4_c_socket_packet_send(int sockfd, + int ifindex, + const unsigned char *dest_haddr, + unsigned char halen, + NDhcp4Outgoing *message); +int n_dhcp4_c_socket_udp_send(int sockfd, NDhcp4Outgoing *message); +int n_dhcp4_c_socket_udp_broadcast(int sockfd, NDhcp4Outgoing *message); +int n_dhcp4_s_socket_packet_send(int sockfd, + int ifindex, + const struct in_addr *src_inaddr, + const unsigned char *dest_haddr, + unsigned char halen, + const struct in_addr *dest_inaddr, + NDhcp4Outgoing *message); +int n_dhcp4_s_socket_udp_send(int sockfd, + const struct in_addr *inaddr_src, + const struct in_addr *inaddr_dest, + NDhcp4Outgoing *message); +int n_dhcp4_s_socket_udp_broadcast(int sockfd, + const struct in_addr *inaddr_src, + NDhcp4Outgoing *message); + +int n_dhcp4_c_socket_packet_recv(int sockfd, + uint8_t *buf, + size_t n_buf, + NDhcp4Incoming **messagep); +int n_dhcp4_c_socket_udp_recv(int sockfd, + uint8_t *buf, + size_t n_buf, + NDhcp4Incoming **messagep); +int n_dhcp4_s_socket_udp_recv(int sockfd, + uint8_t *buf, + size_t n_buf, + NDhcp4Incoming **messagep, + struct sockaddr_in *dest); + +/* client configs */ + +int n_dhcp4_client_config_dup(NDhcp4ClientConfig *config, + NDhcp4ClientConfig **dupp); + +/* client probe configs */ + +int n_dhcp4_client_probe_config_dup(NDhcp4ClientProbeConfig *config, + NDhcp4ClientProbeConfig **dupp); +uint32_t n_dhcp4_client_probe_config_get_random(NDhcp4ClientProbeConfig *config); + +/* client events */ + +int n_dhcp4_c_event_node_new(NDhcp4CEventNode **nodep); +NDhcp4CEventNode *n_dhcp4_c_event_node_free(NDhcp4CEventNode *node); + +/* client connections */ + +int n_dhcp4_c_connection_init(NDhcp4CConnection *connection, + NDhcp4ClientConfig *client_config, + NDhcp4ClientProbeConfig *probe_config, + int fd_epoll); +void n_dhcp4_c_connection_deinit(NDhcp4CConnection *connection); + +int n_dhcp4_c_connection_listen(NDhcp4CConnection *connection); +int n_dhcp4_c_connection_connect(NDhcp4CConnection *connection, + const struct in_addr *client, + const struct in_addr *server); +void n_dhcp4_c_connection_close(NDhcp4CConnection *connection); + +void n_dhcp4_c_connection_get_timeout(NDhcp4CConnection *connection, + uint64_t *timeoutp); + +int n_dhcp4_c_connection_discover_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request); +int n_dhcp4_c_connection_select_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request, + NDhcp4Incoming *offer); +int n_dhcp4_c_connection_reboot_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request, + const struct in_addr *client); +int n_dhcp4_c_connection_renew_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request); +int n_dhcp4_c_connection_rebind_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request); +int n_dhcp4_c_connection_decline_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request, + NDhcp4Incoming *ack, + const char *error); +int n_dhcp4_c_connection_inform_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request); +int n_dhcp4_c_connection_release_new(NDhcp4CConnection *connection, + NDhcp4Outgoing **request, + const char *error); + +int n_dhcp4_c_connection_start_request(NDhcp4CConnection *connection, + NDhcp4Outgoing *request, + uint64_t timestamp); +int n_dhcp4_c_connection_dispatch_timer(NDhcp4CConnection *connection, + uint64_t timestamp); +int n_dhcp4_c_connection_dispatch_io(NDhcp4CConnection *connection, + NDhcp4Incoming **messagep); + +/* clients */ + +int n_dhcp4_client_raise(NDhcp4Client *client, NDhcp4CEventNode **nodep, unsigned int event); +void n_dhcp4_client_arm_timer(NDhcp4Client *client); + +/* client probes */ + +int n_dhcp4_client_probe_new(NDhcp4ClientProbe **probep, + NDhcp4ClientProbeConfig *config, + NDhcp4Client *client, + uint64_t ns_now); + +int n_dhcp4_client_probe_raise(NDhcp4ClientProbe *probe, NDhcp4CEventNode **nodep, unsigned int event); +void n_dhcp4_client_probe_get_timeout(NDhcp4ClientProbe *probe, uint64_t *timeoutp); +int n_dhcp4_client_probe_dispatch_timer(NDhcp4ClientProbe *probe, uint64_t ns_now); +int n_dhcp4_client_probe_dispatch_io(NDhcp4ClientProbe *probe, uint32_t events); +int n_dhcp4_client_probe_transition_select(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, uint64_t ns_now); +int n_dhcp4_client_probe_transition_accept(NDhcp4ClientProbe *probe, NDhcp4Incoming *ack); +int n_dhcp4_client_probe_transition_decline(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, const char *error, uint64_t ns_now); +int n_dhcp4_client_probe_update_mtu(NDhcp4ClientProbe *probe, uint16_t mtu); + +/* client leases */ + +int n_dhcp4_client_lease_new(NDhcp4ClientLease **leasep, NDhcp4Incoming *message); +void n_dhcp4_client_lease_link(NDhcp4ClientLease *lease, NDhcp4ClientProbe *probe); +void n_dhcp4_client_lease_unlink(NDhcp4ClientLease *lease); + +/* server connections */ + +int n_dhcp4_s_connection_init(NDhcp4SConnection *connection, int ifindex); +void n_dhcp4_s_connection_deinit(NDhcp4SConnection *connection); + +void n_dhcp4_s_connection_get_fd(NDhcp4SConnection *connection, int *fdp); +int n_dhcp4_s_connection_dispatch_io(NDhcp4SConnection *connection, NDhcp4Incoming **messagep); + +int n_dhcp4_s_connection_offer_new(NDhcp4SConnection *connection, + NDhcp4Outgoing **replyp, + NDhcp4Incoming *request, + const struct in_addr *server_address, + const struct in_addr *client_address, + uint32_t lifetime); +int n_dhcp4_s_connection_ack_new(NDhcp4SConnection *connection, + NDhcp4Outgoing **replyp, + NDhcp4Incoming *request, + const struct in_addr *server_address, + const struct in_addr *client_address, + uint32_t lifetime); +int n_dhcp4_s_connection_nak_new(NDhcp4SConnection *connection, + NDhcp4Outgoing **replyp, + NDhcp4Incoming *request, + const struct in_addr *server_address); + +int n_dhcp4_s_connection_send_reply(NDhcp4SConnection *connection, + const struct in_addr *server_addr, + NDhcp4Outgoing *reply); + +/* server connection ips */ + +void n_dhcp4_s_connection_ip_init(NDhcp4SConnectionIp *ip, struct in_addr addr); +void n_dhcp4_s_connection_ip_deinit(NDhcp4SConnectionIp *ip); + +void n_dhcp4_s_connection_ip_link(NDhcp4SConnectionIp *ip, NDhcp4SConnection *connection); +void n_dhcp4_s_connection_ip_unlink(NDhcp4SConnectionIp *ip); + +/* inline helpers */ + +static inline void n_dhcp4_outgoing_freep(NDhcp4Outgoing **outgoing) { + if (*outgoing) + n_dhcp4_outgoing_free(*outgoing); +} + +static inline void n_dhcp4_incoming_freep(NDhcp4Incoming **incoming) { + if (*incoming) + n_dhcp4_incoming_free(*incoming); +} + +static inline uint64_t n_dhcp4_gettime(clockid_t clock) { + struct timespec ts; + int r; + + r = clock_gettime(clock, &ts); + c_assert(r >= 0); + + return ts.tv_sec * 1000ULL * 1000ULL * 1000ULL + ts.tv_nsec; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-s-connection.c b/shared/n-dhcp4/src/n-dhcp4-s-connection.c new file mode 100644 index 000000000..71ae0be82 --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-s-connection.c @@ -0,0 +1,411 @@ +/* + * DHCPv4 Server Connection + * + * XXX + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4-private.h" +#include "util/packet.h" + +int n_dhcp4_s_connection_init(NDhcp4SConnection *connection, int ifindex) { + int r; + + *connection = (NDhcp4SConnection)N_DHCP4_S_CONNECTION_NULL(*connection); + + r = n_dhcp4_s_socket_packet_new(&connection->fd_packet); + if (r) + return r; + + r = n_dhcp4_s_socket_udp_new(&connection->fd_udp, ifindex); + if (r) + return r; + + connection->ifindex = ifindex; + + return 0; +} + +void n_dhcp4_s_connection_deinit(NDhcp4SConnection *connection) { + c_assert(!connection->ip); + + if (connection->fd_udp >= 0) { + close(connection->fd_udp); + } + + if (connection->fd_packet >= 0) { + close(connection->fd_packet); + } + + *connection = (NDhcp4SConnection)N_DHCP4_S_CONNECTION_NULL(*connection); +} + +void n_dhcp4_s_connection_get_fd(NDhcp4SConnection *connection, int *fdp) { + *fdp = connection->fd_udp; +} + +static bool n_dhcp4_s_connection_owns_ip(NDhcp4SConnection *connection, struct in_addr addr) { + if (!connection->ip) + return false; + return (connection->ip->ip.s_addr == addr.s_addr); +} + +static int n_dhcp4_s_connection_verify_incoming(NDhcp4SConnection *connection, + NDhcp4Incoming *message, + bool broadcast) { + uint8_t type; + int r; + + r = n_dhcp4_incoming_query_message_type(message, &type); + if (r) { + if (r == N_DHCP4_E_UNSET) + return N_DHCP4_E_MALFORMED; + else + return r; + } + + switch (type) { + case N_DHCP4_MESSAGE_DISCOVER: + message->userdata.type = N_DHCP4_C_MESSAGE_DISCOVER; + break; + case N_DHCP4_MESSAGE_REQUEST: { + struct in_addr server_identifier = {}; + struct in_addr requested_ip = {}; + + r = n_dhcp4_incoming_query_server_identifier(message, &server_identifier); + if (r) { + if (r == N_DHCP4_E_UNSET) { + r = n_dhcp4_incoming_query_requested_ip(message, &requested_ip); + if (r) { + if (r == N_DHCP4_E_UNSET) { + if (broadcast) { + message->userdata.type = N_DHCP4_C_MESSAGE_REBIND; + } else { + message->userdata.type = N_DHCP4_C_MESSAGE_RENEW; + } + } else { + return r; + } + } else { + message->userdata.type = N_DHCP4_C_MESSAGE_REBOOT; + } + } else { + return r; + } + } else { + if (n_dhcp4_s_connection_owns_ip(connection, server_identifier)) { + message->userdata.type = N_DHCP4_C_MESSAGE_SELECT; + } else { + message->userdata.type = N_DHCP4_C_MESSAGE_IGNORE; + } + } + } + break; + case N_DHCP4_MESSAGE_DECLINE: + message->userdata.type = N_DHCP4_C_MESSAGE_DECLINE; + break; + case N_DHCP4_MESSAGE_RELEASE: + message->userdata.type = N_DHCP4_C_MESSAGE_RELEASE; + break; + default: + return N_DHCP4_E_UNEXPECTED; + } + + return 0; +} + +int n_dhcp4_s_connection_dispatch_io(NDhcp4SConnection *connection, NDhcp4Incoming **messagep) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + struct sockaddr_in dest = {}; + int r; + + r = n_dhcp4_s_socket_udp_recv(connection->fd_udp, + connection->buf, + sizeof(connection->buf), + &message, + &dest); + if (r) + return r; + + r = n_dhcp4_s_connection_verify_incoming(connection, + message, + dest.sin_addr.s_addr == INADDR_BROADCAST); + if (r) { + if (r == N_DHCP4_E_MALFORMED || r == N_DHCP4_E_UNEXPECTED) { + *messagep = NULL; + return 0; + } + + return -ENOTRECOVERABLE; + } + + *messagep = message; + message = NULL; + return 0; +} + +/* + * 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. + */ +int n_dhcp4_s_connection_send_reply(NDhcp4SConnection *connection, + const struct in_addr *server_addr, + NDhcp4Outgoing *message) { + NDhcp4Header *header = n_dhcp4_outgoing_get_header(message); + int r; + + if (header->giaddr) { + const struct in_addr giaddr = { header->giaddr }; + + r = n_dhcp4_s_socket_udp_send(connection->fd_udp, + server_addr, + &giaddr, + message); + if (r) + return r; + } else if (header->ciaddr) { + const struct in_addr ciaddr = { header->ciaddr }; + + r = n_dhcp4_s_socket_udp_send(connection->fd_udp, + server_addr, + &ciaddr, + message); + if (r) + return r; + } else if (header->flags & htons(N_DHCP4_MESSAGE_FLAG_BROADCAST)) { + r = n_dhcp4_s_socket_udp_broadcast(connection->fd_udp, + server_addr, + message); + if (r) + return r; + } else { + r = n_dhcp4_s_socket_packet_send(connection->fd_packet, + connection->ifindex, + server_addr, + header->chaddr, + header->hlen, + &(struct in_addr){header->yiaddr}, + message); + if (r) + return r; + } + + return 0; +} + +static void n_dhcp4_s_connection_init_reply_header(NDhcp4SConnection *connection, + NDhcp4Header *request, + NDhcp4Header *reply) { + reply->op = N_DHCP4_OP_BOOTREPLY; + + reply->htype = request->htype; + reply->hlen = request->hlen; + reply->flags = request->flags; + reply->xid = request->xid; + reply->ciaddr = request->ciaddr; + reply->giaddr = request->giaddr; + memcpy(reply->chaddr, request->chaddr, request->hlen); +} + +static int n_dhcp4_s_connection_outgoing_set_yiaddr(NDhcp4Outgoing *message, + uint32_t yiaddr, + uint32_t lifetime) { + uint32_t t1 = lifetime / 2; + uint32_t t2 = ((uint64_t)lifetime * 7) / 8; + struct in_addr addr = { .s_addr = yiaddr }; + int r; + + r = n_dhcp4_outgoing_append_lifetime(message, lifetime); + if (r) + return r; + + r = n_dhcp4_outgoing_append_t1(message, t1); + if (r) + return r; + + r = n_dhcp4_outgoing_append_t2(message, t2); + if (r) + return r; + + n_dhcp4_outgoing_set_yiaddr(message, addr); + + return 0; +} + +static int n_dhcp4_s_connection_new_reply(NDhcp4SConnection *connection, + NDhcp4Outgoing **messagep, + NDhcp4Incoming *request, + uint8_t type, + const struct in_addr *server_address) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL; + uint16_t max_message_size; + uint8_t *client_identifier; + size_t n_client_identifier; + int r; + + r = n_dhcp4_incoming_query_max_message_size(request, &max_message_size); + if (r) + return r; + + r = n_dhcp4_outgoing_new(&message, + max_message_size, + N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME); + if (r) + return r; + + n_dhcp4_s_connection_init_reply_header(connection, + n_dhcp4_incoming_get_header(request), + n_dhcp4_outgoing_get_header(message)); + + r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MESSAGE_TYPE, &type, sizeof(type)); + if (r) + return r; + + r = n_dhcp4_outgoing_append_server_identifier(message, *server_address); + if (r) + return r; + + r = n_dhcp4_incoming_query(request, + N_DHCP4_OPTION_CLIENT_IDENTIFIER, + &client_identifier, + &n_client_identifier); + if (!r) { + r = n_dhcp4_outgoing_append(message, + N_DHCP4_OPTION_CLIENT_IDENTIFIER, + client_identifier, + n_client_identifier); + if (r) + return r; + } else if (r != N_DHCP4_E_UNSET) { + return r; + } + + *messagep = message; + message = NULL; + return 0; +} + +int n_dhcp4_s_connection_offer_new(NDhcp4SConnection *connection, + NDhcp4Outgoing **replyp, + NDhcp4Incoming *request, + const struct in_addr *server_address, + const struct in_addr *client_address, + uint32_t lifetime) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL; + int r; + + r = n_dhcp4_s_connection_new_reply(connection, + &reply, + request, + N_DHCP4_MESSAGE_OFFER, + server_address); + if (r) + return r; + + r = n_dhcp4_s_connection_outgoing_set_yiaddr(reply, + client_address->s_addr, + lifetime); + if (r) + return r; + + *replyp = reply; + reply = NULL; + return 0; +} + +int n_dhcp4_s_connection_ack_new(NDhcp4SConnection *connection, + NDhcp4Outgoing **replyp, + NDhcp4Incoming *request, + const struct in_addr *server_address, + const struct in_addr *client_address, + uint32_t lifetime) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL; + int r; + + r = n_dhcp4_s_connection_new_reply(connection, + &reply, + request, + N_DHCP4_MESSAGE_ACK, + server_address); + if (r) + return r; + + r = n_dhcp4_s_connection_outgoing_set_yiaddr(reply, + client_address->s_addr, + lifetime); + if (r) + return r; + + *replyp = reply; + reply = NULL; + return 0; +} + +int n_dhcp4_s_connection_nak_new(NDhcp4SConnection *connection, + NDhcp4Outgoing **replyp, + NDhcp4Incoming *request, + const struct in_addr *server_address) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL; + int r; + + r = n_dhcp4_s_connection_new_reply(connection, + &reply, + request, + N_DHCP4_MESSAGE_NAK, + server_address); + if (r) + return r; + + /* + * The RFC is a bit unclear on how NAK should be sent, on the + * one hand it says that they should be unconditinoally broadcast + * (unless going through a relay agent), on the other, when they + * do go through a relay agent, they will not be. We treat them + * as any other reply and only broadcast when the broadcast bit + * is set. + */ + + *replyp = reply; + reply = NULL; + return 0; +} + +void n_dhcp4_s_connection_ip_init(NDhcp4SConnectionIp *ip, struct in_addr addr) { + *ip = (NDhcp4SConnectionIp)N_DHCP4_S_CONNECTION_IP_NULL(*ip); + ip->ip = addr; +} + +void n_dhcp4_s_connection_ip_deinit(NDhcp4SConnectionIp *ip) { + c_assert(!ip->connection); + *ip = (NDhcp4SConnectionIp)N_DHCP4_S_CONNECTION_IP_NULL(*ip); +} + +void n_dhcp4_s_connection_ip_link(NDhcp4SConnectionIp *ip, NDhcp4SConnection *connection) { + c_assert(!connection->ip); + c_assert(!ip->connection); + + connection->ip = ip; + ip->connection = connection; +} + +void n_dhcp4_s_connection_ip_unlink(NDhcp4SConnectionIp *ip) { + ip->connection->ip = NULL; + ip->connection = NULL; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-s-lease.c b/shared/n-dhcp4/src/n-dhcp4-s-lease.c new file mode 100644 index 000000000..cb8aabf91 --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-s-lease.c @@ -0,0 +1,103 @@ +/* + * XXX + */ + +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" + +/** + * n_dhcp4_server_lease_new() - XXX + */ +int n_dhcp4_server_lease_new(NDhcp4ServerLease **leasep, NDhcp4Incoming *message) { + _c_cleanup_(n_dhcp4_server_lease_unrefp) NDhcp4ServerLease *lease = NULL; + + c_assert(leasep); + + lease = malloc(sizeof(*lease)); + if (!lease) + return -ENOMEM; + + *lease = (NDhcp4ServerLease)N_DHCP4_SERVER_LEASE_NULL(*lease); + + lease->request = message; + + *leasep = lease; + lease = NULL; + return 0; +} + +static void n_dhcp4_server_lease_free(NDhcp4ServerLease *lease) { + c_assert(!lease->server); + + c_list_unlink(&lease->server_link); + + n_dhcp4_incoming_free(lease->request); + free(lease); +} + +/** + * n_dhcp4_server_lease_ref() - XXX + */ +_c_public_ NDhcp4ServerLease *n_dhcp4_server_lease_ref(NDhcp4ServerLease *lease) { + if (lease) + ++lease->n_refs; + return lease; +} + +/** + * n_dhcp4_server_lease_unref() - XXX + */ +_c_public_ NDhcp4ServerLease *n_dhcp4_server_lease_unref(NDhcp4ServerLease *lease) { + if (lease && !--lease->n_refs) + n_dhcp4_server_lease_free(lease); + return NULL; +} + +/** + * n_dhcp4_server_lease_query() - XXX + */ +_c_public_ int n_dhcp4_server_lease_query(NDhcp4ServerLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap) { + switch (option) { + case N_DHCP4_OPTION_PAD: + case N_DHCP4_OPTION_REQUESTED_IP_ADDRESS: + case N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME: + case N_DHCP4_OPTION_OVERLOAD: + case N_DHCP4_OPTION_MESSAGE_TYPE: + case N_DHCP4_OPTION_SERVER_IDENTIFIER: + case N_DHCP4_OPTION_PARAMETER_REQUEST_LIST: + case N_DHCP4_OPTION_ERROR_MESSAGE: + case N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE: + case N_DHCP4_OPTION_RENEWAL_T1_TIME: + case N_DHCP4_OPTION_REBINDING_T2_TIME: + case N_DHCP4_OPTION_END: + return N_DHCP4_E_INTERNAL; + } + + return n_dhcp4_incoming_query(lease->request, option, datap, n_datap); +} + +_c_public_ int n_dhcp4_server_lease_append(NDhcp4ServerLease *lease, uint8_t option, uint8_t *data, size_t n_data) { + /* XXX */ + return -ENOTRECOVERABLE; +} + +_c_public_ int n_dhcp4_server_lease_offer(NDhcp4ServerLease *lease) { + /* XXX */ + return -ENOTRECOVERABLE; +} + +_c_public_ int n_dhcp4_server_lease_ack(NDhcp4ServerLease *lease) { + /* XXX */ + return -ENOTRECOVERABLE; +} + +_c_public_ int n_dhcp4_server_lease_nack(NDhcp4ServerLease *lease) { + /* XXX */ + return -ENOTRECOVERABLE; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-server.c b/shared/n-dhcp4/src/n-dhcp4-server.c new file mode 100644 index 000000000..7b971eeb2 --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-server.c @@ -0,0 +1,241 @@ +/* + * Server Side of the Dynamic Host Configuration Protocol for IPv4 + * + * XXX + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" +#include "util/packet.h" + +/** + * n_dhcp4_server_config_new() - XXX + */ +_c_public_ int n_dhcp4_server_config_new(NDhcp4ServerConfig **configp) { + _c_cleanup_(n_dhcp4_server_config_freep) NDhcp4ServerConfig *config = NULL; + + config = calloc(1, sizeof(*config)); + if (!config) + return -ENOMEM; + + *config = (NDhcp4ServerConfig)N_DHCP4_SERVER_CONFIG_NULL(*config); + + *configp = config; + config = NULL; + return 0; +} + +/** + * n_dhcp4_server_config_free() - XXX + */ +_c_public_ NDhcp4ServerConfig *n_dhcp4_server_config_free(NDhcp4ServerConfig *config) { + if (!config) + return NULL; + + free(config); + + return NULL; +} + +/** + * n_dhcp4_server_config_set_ifindex() - XXX + */ +_c_public_ void n_dhcp4_server_config_set_ifindex(NDhcp4ServerConfig *config, int ifindex) { + config->ifindex = ifindex; +} + +/** + * n_dhcp4_s_event_node_new() - XXX + */ +int n_dhcp4_s_event_node_new(NDhcp4SEventNode **nodep) { + NDhcp4SEventNode *node; + + node = calloc(1, sizeof(*node)); + if (!node) + return -ENOMEM; + + *node = (NDhcp4SEventNode)N_DHCP4_S_EVENT_NODE_NULL(*node); + + *nodep = node; + return 0; +} + +/** + * n_dhcp4_s_event_node_free() - XXX + */ +NDhcp4SEventNode *n_dhcp4_s_event_node_free(NDhcp4SEventNode *node) { + if (!node) + return NULL; + + c_list_unlink(&node->server_link); + free(node); + + return NULL; +} + +/** + * n_dhcp4_server_new() - XXX + */ +_c_public_ int n_dhcp4_server_new(NDhcp4Server **serverp, NDhcp4ServerConfig *config) { + _c_cleanup_(n_dhcp4_server_unrefp) NDhcp4Server *server = NULL; + int r; + + c_assert(serverp); + + server = malloc(sizeof(*server)); + if (!server) + return -ENOMEM; + + *server = (NDhcp4Server)N_DHCP4_SERVER_NULL(*server); + + r = n_dhcp4_s_connection_init(&server->connection, config->ifindex); + if (r) + return r; + + *serverp = server; + server = NULL; + return 0; +} + +static void n_dhcp4_server_free(NDhcp4Server *server) { + NDhcp4SEventNode *node, *t_node; + + c_list_for_each_entry_safe(node, t_node, &server->event_list, server_link) + n_dhcp4_s_event_node_free(node); + + free(server); +} + +/** + * n_dhcp4_server_ref() - XXX + */ +_c_public_ NDhcp4Server *n_dhcp4_server_ref(NDhcp4Server *server) { + if (server) + ++server->n_refs; + return server; +} + +/** + * n_dhcp4_server_unref() - XXX + */ +_c_public_ NDhcp4Server *n_dhcp4_server_unref(NDhcp4Server *server) { + if (server && !--server->n_refs) + n_dhcp4_server_free(server); + return NULL; +} + +/** + * n_dhcp4_server_raise() - XXX + */ +int n_dhcp4_server_raise(NDhcp4Server *server, NDhcp4SEventNode **nodep, unsigned int event) { + NDhcp4SEventNode *node; + int r; + + r = n_dhcp4_s_event_node_new(&node); + if (r) + return r; + + node->event.event = event; + c_list_link_tail(&server->event_list, &node->server_link); + + if (nodep) + *nodep = node; + return 0; +} + +/** + * n_dhcp4_server_get_fd() - XXX + */ +_c_public_ void n_dhcp4_server_get_fd(NDhcp4Server *server, int *fdp) { + n_dhcp4_s_connection_get_fd(&server->connection, fdp); +} + +/** + * n_dhcp4_server_dispatch() - XXX + */ +_c_public_ int n_dhcp4_server_dispatch(NDhcp4Server *server) { + int r; + + for (unsigned int i = 0; i < 128; ++i) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + + r = n_dhcp4_s_connection_dispatch_io(&server->connection, &message); + if (r) { + if (r == N_DHCP4_E_AGAIN) + return 0; + return r; + } + } + + return N_DHCP4_E_PREEMPTED; +} + +/** + * n_dhcp4_server_pop_event() - XXX + */ +_c_public_ int n_dhcp4_server_pop_event(NDhcp4Server *server, NDhcp4ServerEvent **eventp) { + NDhcp4SEventNode *node, *t_node; + + c_list_for_each_entry_safe(node, t_node, &server->event_list, server_link) { + if (node->is_public) { + n_dhcp4_s_event_node_free(node); + continue; + } + + node->is_public = true; + *eventp = &node->event; + return 0; + } + + *eventp = NULL; + return 0; +} + +/** + * n_dhcp4_server_add_ip() - XXX + */ +_c_public_ int n_dhcp4_server_add_ip(NDhcp4Server *server, NDhcp4ServerIp **ipp, struct in_addr addr) { + _c_cleanup_(n_dhcp4_server_ip_freep) NDhcp4ServerIp *ip = NULL; + + /* XXX: support more than one address */ + if (server->connection.ip) + return -EBUSY; + + ip = malloc(sizeof(*ip)); + if (!ip) + return -ENOMEM; + + *ip = (NDhcp4ServerIp)N_DHCP4_SERVER_IP_NULL(*ip); + + n_dhcp4_s_connection_ip_init(&ip->ip, addr); + n_dhcp4_s_connection_ip_link(&ip->ip, &server->connection); + + *ipp = ip; + ip = NULL; + return 0; +} + +/** + * n_dhcp4_server_ip_free() - XXX + */ +_c_public_ NDhcp4ServerIp *n_dhcp4_server_ip_free(NDhcp4ServerIp *ip) { + if (!ip) + return NULL; + + n_dhcp4_s_connection_ip_unlink(&ip->ip); + n_dhcp4_s_connection_ip_deinit(&ip->ip); + + free(ip); + return NULL; +} diff --git a/shared/n-dhcp4/src/n-dhcp4-socket.c b/shared/n-dhcp4/src/n-dhcp4-socket.c new file mode 100644 index 000000000..0bcc2419d --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4-socket.c @@ -0,0 +1,656 @@ +/* + * DHCP specific low-level socket helpers + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4-private.h" +#include "util/packet.h" +#include "util/socket.h" + +/** + * n_dhcp4_c_socket_packet_new() - create a new DHCP4 client packet socket + * @sockfdp: return argumnet for the new socket + * @ifindex: interface index to bind to + * + * Create a new AF_PACKET/SOCK_DGRAM socket usable to listen to and send DHCP client + * packets before an IP address has been configured. + * + * Only unfragmented DHCP packets from a server to a client destined for the given + * ifindex is returned. + * + * Return: 0 on success, or a negative error code on failure. + */ +int n_dhcp4_c_socket_packet_new(int *sockfdp, int ifindex) { + _c_cleanup_(c_closep) int sockfd = -1; + struct sock_filter filter[] = { + /* + * IP + * + * Check + * - UDP + * - Unfragmented + * - Large enough to fit the DHCP header + * + * Leave X the size of the IP header, for future indirect reads. + */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct iphdr, 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(struct iphdr, frag_off)), /* A <- Flags */ + BPF_STMT(BPF_ALU + BPF_AND + BPF_K, ntohs(IP_MF | IP_OFFMASK)), /* A <- A & (IP_MF | IP_OFFMASK) */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0, 1, 0), /* fragmented packet ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0), /* X <- IP header length */ + BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0), /* A <- packet length */ + BPF_STMT(BPF_ALU + BPF_SUB + BPF_X, 0), /* A -= X */ + BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct udphdr) + sizeof(NDhcp4Message), 1, 0), /* packet >= DHCPPacket ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + /* + * UDP + * + * Check + * - DHCP client port + * + * Leave X the size of IP and UDP headers, for future indirect reads. + */ + BPF_STMT(BPF_LD + BPF_H + BPF_IND, offsetof(struct udphdr, dest)), /* A <- UDP destination port */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_NETWORK_CLIENT_PORT, 1, 0), /* UDP destination port == DHCP client port ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)), /* A <- size of UDP header */ + BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0), /* A += X */ + BPF_STMT(BPF_MISC + BPF_TAX, 0), /* X <- A */ + + /* + * DHCP + * + * Check + * - BOOTREPLY (from server to client) + * - DHCP magic cookie + */ + BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)), /* A <- DHCP op */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREPLY, 1, 0), /* op == BOOTREPLY ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)), /* A <- DHCP magic cookie */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0), /* cookie == DHCP magic cookie ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_RET + BPF_K, 65535), /* return all */ + }; + struct sock_fprog fprog = { + .filter = filter, + .len = sizeof(filter) / sizeof(filter[0]), + }; + struct sockaddr_ll addr = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_IP), + .sll_ifindex = ifindex, + }; + int r, on = 1; + + sockfd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sockfd < 0) + return -errno; + + r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)); + if (r < 0) + return -errno; + + /* We need the flag that tells us if the checksum is correct. */ + r = setsockopt(sockfd, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on)); + if (r < 0) + return -errno; + + r = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); + if (r < 0) + return -errno; + + *sockfdp = sockfd; + sockfd = -1; + return 0; +} + +/** + * n_dhcp4_c_socket_udp_new() - create a new DHCP4 client UDP socket + * @sockfdp: return argumnet for the new socket + * @ifindex: interface index to bind to + * @client_addr: client address to bind to + * @server_addr: server address to connect to + * + * Create a new AF_INET/SOCK_DGRAM socket usable to listen to and send DHCP client + * packets. + * + * The client address given in @addr must be configured on the interface @ifindex + * before the socket is created. + * + * Return: 0 on success, or a negative error code on failure. + */ +int n_dhcp4_c_socket_udp_new(int *sockfdp, + int ifindex, + const struct in_addr *client_addr, + const struct in_addr *server_addr) { + _c_cleanup_(c_closep) int sockfd = -1; + struct sock_filter filter[] = { + /* + * IP/UDP + * + * Set X to the size of IP and UDP headers, for future indirect reads. + */ + BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0), /* X <- IP header length */ + BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)), /* A <- size of UDP header */ + BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0), /* A += X */ + BPF_STMT(BPF_MISC + BPF_TAX, 0), /* X <- A */ + + /* + * DHCP + * + * Check + * - BOOTREPLY (from server to client) + * - DHCP magic cookie + */ + BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)), /* A <- DHCP op */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREPLY, 1, 0), /* op == BOOTREPLY ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)), /* A <- DHCP magic cookie */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0), /* cookie == DHCP magic cookie ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_RET + BPF_K, 65535), /* return all */ + }; + struct sock_fprog fprog = { + .filter = filter, + .len = sizeof(filter) / sizeof(filter[0]), + }; + struct sockaddr_in saddr = { + .sin_family = AF_INET, + .sin_addr = *client_addr, + .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT), + }; + struct sockaddr_in daddr = { + .sin_family = AF_INET, + .sin_addr = *server_addr, + .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT), + }; + int r, tos = IPTOS_CLASS_CS6, on = 1; + + sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sockfd < 0) + return -errno; + + r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)); + if (r < 0) + return -errno; + + r = socket_bind_if(sockfd, ifindex); + if (r) + return r; + + r = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); + if (r < 0) + return -errno; + + r = setsockopt(sockfd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + if (r < 0) + return -errno; + + r = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); + if (r < 0) + return -errno; + + r = connect(sockfd, (struct sockaddr*)&daddr, sizeof(daddr)); + if (r < 0) + return -errno; + + *sockfdp = sockfd; + sockfd = -1; + return 0; +} + +/** + * n_dhcp4_s_socket_packet_new() - create a new DHCP4 server packet socket + * @sockfdp: return argumnet for the new socket + * + * Create a new AF_PACKET/SOCK_DGRAM socket usable to send DHCP packets to clients + * before they have an IP address configured, on the given interface. + * + * Return: 0 on success, or a negative error code on failure. + */ +int n_dhcp4_s_socket_packet_new(int *sockfdp) { + _c_cleanup_(c_closep) int sockfd = -1; + + sockfd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sockfd < 0) + return -errno; + + *sockfdp = sockfd; + sockfd = -1; + return 0; +} + +/** + * n_dhcp4_s_socket_udp_new() - create a new DHCP4 server UDP socket + * @sockfdp: return argumnet for the new socket + * @ifindex: intercafe index to bind to + * + * Create a new AF_INET/SOCK_DGRAM socket usable to listen to DHCP server packets, + * on the given interface. + * + * Return: 0 on success, or a negative error code on failure. + */ +int n_dhcp4_s_socket_udp_new(int *sockfdp, int ifindex) { + _c_cleanup_(c_closep) int sockfd = -1; + struct sock_filter filter[] = { + /* + * IP/UDP + * + * Set X to the size of IP and UDP headers, for future indirect reads. + */ + BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0), /* X <- IP header length */ + BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)), /* A <- size of UDP header */ + BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0), /* A += X */ + BPF_STMT(BPF_MISC + BPF_TAX, 0), /* X <- A */ + + /* + * DHCP + * + * Check + * - BOOTREQUEST (from client to server) + * - DHCP magic cookie + */ + + BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)), /* A <- DHCP op */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREQUEST, 1, 0), /* op == BOOTREQUEST ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)), /* A <- DHCP magic cookie */ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0), /* cookie == DHCP magic cookie ? */ + BPF_STMT(BPF_RET + BPF_K, 0), /* ignore */ + + BPF_STMT(BPF_RET + BPF_K, 65535), /* return all */ + }; + struct sock_fprog fprog = { + .filter = filter, + .len = sizeof(filter) / sizeof(filter[0]), + }; + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr = { INADDR_ANY }, + .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT), + }; + int r, tos = IPTOS_CLASS_CS6, on = 1; + + sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sockfd < 0) + return -errno; + + r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)); + if (r < 0) + return -errno; + + r = socket_bind_if(sockfd, ifindex); + if (r) + return r; + + r = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); + if (r < 0) + return -errno; + + r = setsockopt(sockfd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + if (r < 0) + return -errno; + + r = setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on)); + if (r < 0) + return -errno; + + r = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); + if (r < 0) + return -errno; + + *sockfdp = sockfd; + sockfd = -1; + return 0; +} + +static int n_dhcp4_socket_packet_send(int sockfd, + int ifindex, + const struct sockaddr_in *src_paddr, + const unsigned char *dest_haddr, + unsigned char halen, + const struct sockaddr_in *dest_paddr, + NDhcp4Outgoing *message) { + struct packet_sockaddr_ll haddr = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_IP), + .sll_ifindex = ifindex, + .sll_halen = halen, + }; + const void *buf; + size_t n_buf, len; + int r; + + c_assert(halen <= sizeof(haddr.sll_addr)); + + memcpy(haddr.sll_addr, dest_haddr, halen); + + n_buf = n_dhcp4_outgoing_get_raw(message, &buf); + + r = packet_sendto_udp(sockfd, buf, n_buf, &len, src_paddr, &haddr, dest_paddr); + if (r < 0) { + if (r == -EAGAIN || r == -ENOBUFS) + return N_DHCP4_E_DROPPED; + else if (r == -ENETDOWN || r == -ENXIO) + return N_DHCP4_E_DOWN; + else + return r; + } else if (len != n_buf) { + return N_DHCP4_E_DROPPED; + } + + return 0; +} + +/** + * n_dhcp4_c_socket_packet_send() - XXX + */ +int n_dhcp4_c_socket_packet_send(int sockfd, + int ifindex, + const unsigned char *dest_haddr, + unsigned char halen, + NDhcp4Outgoing *message) { + struct sockaddr_in src_paddr = { + .sin_family = AF_INET, + .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT), + .sin_addr = { INADDR_ANY }, + }; + struct sockaddr_in dest_paddr = { + .sin_family = AF_INET, + .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT), + .sin_addr = { INADDR_BROADCAST } + }; + + return n_dhcp4_socket_packet_send(sockfd, + ifindex, + &src_paddr, + dest_haddr, + halen, + &dest_paddr, + message); +} + +/** + * n_dhcp4_c_socket_udp_send() - XXX + */ +int n_dhcp4_c_socket_udp_send(int sockfd, + NDhcp4Outgoing *message) { + const void *buf; + size_t n_buf; + ssize_t len; + + n_buf = n_dhcp4_outgoing_get_raw(message, &buf); + + len = send(sockfd, buf, n_buf, 0); + if (len < 0) { + if (errno == EAGAIN || errno == ENOBUFS) + return N_DHCP4_E_DROPPED; + else if (errno == ENETDOWN || errno == ENXIO) + return N_DHCP4_E_DOWN; + else + return -errno; + } else if ((size_t)len != n_buf) + return N_DHCP4_E_DROPPED; + + return 0; +} + +/** + * n_dhcp4_c_socket_udp_broadcast() - XXX + */ +int n_dhcp4_c_socket_udp_broadcast(int sockfd, NDhcp4Outgoing *message) { + struct sockaddr_in sockaddr_dest = { + .sin_family = AF_INET, + .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT), + .sin_addr = { INADDR_BROADCAST }, + }; + const void *buf; + size_t n_buf; + ssize_t len; + + n_buf = n_dhcp4_outgoing_get_raw(message, &buf); + + len = sendto(sockfd, + buf, + n_buf, + 0, + (struct sockaddr*)&sockaddr_dest, + sizeof(sockaddr_dest)); + if (len < 0) { + if (errno == EAGAIN || errno == ENOBUFS) + return N_DHCP4_E_DROPPED; + else if (errno == ENETDOWN || errno == ENXIO) + return N_DHCP4_E_DOWN; + else + return -errno; + } else if ((size_t)len != n_buf) + return N_DHCP4_E_DROPPED; + + return 0; +} + +/** + * n_dhcp4_s_socket_packet_send() - XXX + */ +int n_dhcp4_s_socket_packet_send(int sockfd, + int ifindex, + const struct in_addr *src_inaddr, + const unsigned char *dest_haddr, + unsigned char halen, + const struct in_addr *dest_inaddr, + NDhcp4Outgoing *message) { + struct sockaddr_in src_paddr = { + .sin_family = AF_INET, + .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT), + .sin_addr = *src_inaddr, + }; + struct sockaddr_in dest_paddr = { + .sin_family = AF_INET, + .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT), + .sin_addr = *dest_inaddr, + }; + + return n_dhcp4_socket_packet_send(sockfd, + ifindex, + &src_paddr, + dest_haddr, + halen, + &dest_paddr, + message); +} + +/** + * n_dhcp4_s_socket_udp_send() - XXX + */ +int n_dhcp4_s_socket_udp_send(int sockfd, + const struct in_addr *inaddr_src, + const struct in_addr *inaddr_dest, + NDhcp4Outgoing *message) { + struct sockaddr_in sockaddr_dest = { + .sin_family = AF_INET, + .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT), + .sin_addr = *inaddr_dest, + }; + struct iovec iov = {}; + union { + struct cmsghdr align; /* ensure correct stack alignment */ + char buf[CMSG_SPACE(sizeof(struct in_pktinfo))]; + } control = {}; + struct in_pktinfo pktinfo = { + .ipi_spec_dst = *inaddr_src, + }; + struct msghdr msg = { + .msg_name = (void*)&sockaddr_dest, + .msg_namelen = sizeof(sockaddr_dest), + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control.buf, + .msg_controllen = sizeof(control.buf), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + ssize_t len; + + cmsg->cmsg_level = IPPROTO_IP; + cmsg->cmsg_type = IP_PKTINFO; + cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); + memcpy(CMSG_DATA(cmsg), &pktinfo, sizeof(pktinfo)); + + iov.iov_len = n_dhcp4_outgoing_get_raw(message, (const void **)&iov.iov_base); + + len = sendmsg(sockfd, &msg, 0); + if (len < 0) { + if (errno == EAGAIN || errno == ENOBUFS) + return N_DHCP4_E_DROPPED; + else if (errno == ENETDOWN || errno == ENXIO) + return N_DHCP4_E_DOWN; + else + return -errno; + } else if ((size_t)len != iov.iov_len) + return N_DHCP4_E_DROPPED; + + return 0; +} + +int n_dhcp4_s_socket_udp_broadcast(int sockfd, + const struct in_addr *inaddr_src, + NDhcp4Outgoing *message) { + return n_dhcp4_s_socket_udp_send(sockfd, + inaddr_src, + &(const struct in_addr){INADDR_BROADCAST}, + message); +} + +int n_dhcp4_c_socket_packet_recv(int sockfd, + uint8_t *buf, + size_t n_buf, + NDhcp4Incoming **messagep) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + size_t len; + int r; + + r = packet_recv_udp(sockfd, buf, n_buf, &len); + if (r < 0) { + if (r == -ENETDOWN) + return N_DHCP4_E_DOWN; + else if (r == -EAGAIN) + return N_DHCP4_E_AGAIN; + else + return -errno; + } else if (len == 0) { + return N_DHCP4_E_MALFORMED; + } + + r = n_dhcp4_incoming_new(&message, buf, len); + if (r) + return r; + + *messagep = message; + message = NULL; + return 0; +} + +static int n_dhcp4_socket_udp_recv(int sockfd, + uint8_t *buf, + size_t n_buf, + NDhcp4Incoming **messagep, + struct in_pktinfo *pktinfo) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + struct iovec iov = { + .iov_base = buf, + .iov_len = n_buf, + }; + uint8_t cmsgbuf[CMSG_LEN(sizeof(struct in_pktinfo))]; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cmsgbuf, + .msg_controllen = sizeof(cmsgbuf), + }; + ssize_t len; + int r; + + len = recvmsg(sockfd, &msg, MSG_TRUNC); + if (len < 0) { + if (errno == ENETDOWN) + return N_DHCP4_E_DOWN; + else if (errno == EAGAIN) + return N_DHCP4_E_AGAIN; + else + return -errno; + } else if (len == 0 || (size_t)len > n_buf) { + return N_DHCP4_E_MALFORMED; + } + + r = n_dhcp4_incoming_new(&message, buf, len); + if (r) + return r; + + if (pktinfo) { + struct cmsghdr *cmsg; + + cmsg = CMSG_FIRSTHDR(&msg); + c_assert(cmsg); + c_assert(cmsg->cmsg_level == IPPROTO_IP); + c_assert(cmsg->cmsg_type == IP_PKTINFO); + c_assert(cmsg->cmsg_len == CMSG_LEN(sizeof(struct in_pktinfo))); + + memcpy(pktinfo, (void*)CMSG_DATA(cmsg), sizeof(struct in_pktinfo)); + } + + *messagep = message; + message = NULL; + return 0; +} + +int n_dhcp4_c_socket_udp_recv(int sockfd, + uint8_t *buf, + size_t n_buf, + NDhcp4Incoming **messagep) { + return n_dhcp4_socket_udp_recv(sockfd, buf, n_buf, messagep, NULL); +} + +int n_dhcp4_s_socket_udp_recv(int sockfd, + uint8_t *buf, + size_t n_buf, + NDhcp4Incoming **messagep, + struct sockaddr_in *dest) { + struct in_pktinfo pktinfo = {}; + int r; + + r = n_dhcp4_socket_udp_recv(sockfd, buf, n_buf, messagep, &pktinfo); + if (r) + return r; + + if (dest) { + dest->sin_family = AF_INET; + dest->sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT); + dest->sin_addr.s_addr = pktinfo.ipi_addr.s_addr; + } + + return 0; +} diff --git a/shared/n-dhcp4/src/n-dhcp4.h b/shared/n-dhcp4/src/n-dhcp4.h new file mode 100644 index 000000000..58a3cb803 --- /dev/null +++ b/shared/n-dhcp4/src/n-dhcp4.h @@ -0,0 +1,286 @@ +#pragma once + +/* + * Dynamic Host Configuration Protocol for IPv4 + * + * This is the public header of the n-dhcp4 library, implementing IPv4 Dynamic + * Host Configuration Protocol as described in RFC-2132. This header defines + * the public API and all entry points of n-dhcp4. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +typedef struct NDhcp4Client NDhcp4Client; +typedef struct NDhcp4ClientConfig NDhcp4ClientConfig; +typedef struct NDhcp4ClientEvent NDhcp4ClientEvent; +typedef struct NDhcp4ClientLease NDhcp4ClientLease; +typedef struct NDhcp4ClientProbe NDhcp4ClientProbe; +typedef struct NDhcp4ClientProbeConfig NDhcp4ClientProbeConfig; +typedef struct NDhcp4Server NDhcp4Server; +typedef struct NDhcp4ServerConfig NDhcp4ServerConfig; +typedef struct NDhcp4ServerEvent NDhcp4ServerEvent; +typedef struct NDhcp4ServerIp NDhcp4ServerIp; +typedef struct NDhcp4ServerLease NDhcp4ServerLease; + +#define N_DHCP4_CLIENT_START_DELAY_RFC2131 (UINT64_C(9000)) + +enum { + _N_DHCP4_E_SUCCESS, + + N_DHCP4_E_PREEMPTED, + N_DHCP4_E_INTERNAL, + + N_DHCP4_E_INVALID_IFINDEX, + N_DHCP4_E_INVALID_TRANSPORT, + N_DHCP4_E_INVALID_ADDRESS, + N_DHCP4_E_INVALID_CLIENT_ID, + N_DHCP4_E_DUPLICATE_OPTION, + N_DHCP4_E_UNSET, + + _N_DHCP4_E_N, +}; + +enum { + N_DHCP4_TRANSPORT_ETHERNET, + N_DHCP4_TRANSPORT_INFINIBAND, + _N_DHCP4_TRANSPORT_N, +}; + +enum { + N_DHCP4_CLIENT_EVENT_DOWN, + N_DHCP4_CLIENT_EVENT_OFFER, + N_DHCP4_CLIENT_EVENT_GRANTED, + N_DHCP4_CLIENT_EVENT_RETRACTED, + N_DHCP4_CLIENT_EVENT_EXTENDED, + N_DHCP4_CLIENT_EVENT_EXPIRED, + N_DHCP4_CLIENT_EVENT_CANCELLED, + _N_DHCP4_CLIENT_EVENT_N, +}; + +enum { + N_DHCP4_SERVER_EVENT_DOWN, + N_DHCP4_SERVER_EVENT_DISCOVER, + N_DHCP4_SERVER_EVENT_REQUEST, + N_DHCP4_SERVER_EVENT_RENEW, + N_DHCP4_SERVER_EVENT_DECLINE, + N_DHCP4_SERVER_EVENT_RELEASE, + _N_DHCP4_SERVER_EVENT_N, +}; + +struct NDhcp4ClientEvent { + unsigned int event; + union { + struct { + } down; + struct { + NDhcp4ClientProbe *probe; + NDhcp4ClientLease *lease; + } offer, granted, extended; + struct { + NDhcp4ClientProbe *probe; + } retracted, expired, cancelled; + }; +}; + +struct NDhcp4ServerEvent { + unsigned int event; + union { + struct { + } down; + struct { + NDhcp4ServerLease *lease; + } discover, request, decline, release; + }; +}; + +/* client configs */ + +int n_dhcp4_client_config_new(NDhcp4ClientConfig **configp); +NDhcp4ClientConfig *n_dhcp4_client_config_free(NDhcp4ClientConfig *config); + +void n_dhcp4_client_config_set_ifindex(NDhcp4ClientConfig *config, int ifindex); +void n_dhcp4_client_config_set_transport(NDhcp4ClientConfig *config, unsigned int transport); +void n_dhcp4_client_config_set_request_broadcast(NDhcp4ClientConfig *config, bool request_broadcast); +void n_dhcp4_client_config_set_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac); +void n_dhcp4_client_config_set_broadcast_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac); +int n_dhcp4_client_config_set_client_id(NDhcp4ClientConfig *config, const uint8_t *id, size_t n_id); + +/* client-probe configs */ + +int n_dhcp4_client_probe_config_new(NDhcp4ClientProbeConfig **configp); +NDhcp4ClientProbeConfig *n_dhcp4_client_probe_config_free(NDhcp4ClientProbeConfig *config); + +void n_dhcp4_client_probe_config_set_inform_only(NDhcp4ClientProbeConfig *config, bool inform_only); +void n_dhcp4_client_probe_config_set_init_reboot(NDhcp4ClientProbeConfig *config, bool init_reboot); +void n_dhcp4_client_probe_config_set_requested_ip(NDhcp4ClientProbeConfig *config, struct in_addr ip); +void n_dhcp4_client_probe_config_set_start_delay(NDhcp4ClientProbeConfig *config, uint64_t msecs); +void n_dhcp4_client_probe_config_request_option(NDhcp4ClientProbeConfig *config, uint8_t option); +int n_dhcp4_client_probe_config_append_option(NDhcp4ClientProbeConfig *config, + uint8_t option, + const void *data, + uint8_t n_data); + +/* clients */ + +int n_dhcp4_client_new(NDhcp4Client **clientp, NDhcp4ClientConfig *config); +NDhcp4Client *n_dhcp4_client_ref(NDhcp4Client *client); +NDhcp4Client *n_dhcp4_client_unref(NDhcp4Client *client); + +void n_dhcp4_client_get_fd(NDhcp4Client *client, int *fdp); +int n_dhcp4_client_dispatch(NDhcp4Client *client); +int n_dhcp4_client_pop_event(NDhcp4Client *client, NDhcp4ClientEvent **eventp); + +int n_dhcp4_client_update_mtu(NDhcp4Client *client, uint16_t mtu); + +int n_dhcp4_client_probe(NDhcp4Client *client, + NDhcp4ClientProbe **probep, + NDhcp4ClientProbeConfig *config); + +/* client probes */ + +NDhcp4ClientProbe *n_dhcp4_client_probe_free(NDhcp4ClientProbe *probe); + +void n_dhcp4_client_probe_set_userdata(NDhcp4ClientProbe *probe, void *userdata); +void n_dhcp4_client_probe_get_userdata(NDhcp4ClientProbe *probe, void **userdatap); + +/* client leases */ + +NDhcp4ClientLease *n_dhcp4_client_lease_ref(NDhcp4ClientLease *lease); +NDhcp4ClientLease *n_dhcp4_client_lease_unref(NDhcp4ClientLease *lease); + +void n_dhcp4_client_lease_get_yiaddr(NDhcp4ClientLease *lease, struct in_addr *yiaddr); +void n_dhcp4_client_lease_get_lifetime(NDhcp4ClientLease *lease, uint64_t *ns_lifetimep); +int n_dhcp4_client_lease_query(NDhcp4ClientLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap); + +int n_dhcp4_client_lease_select(NDhcp4ClientLease *lease); +int n_dhcp4_client_lease_accept(NDhcp4ClientLease *lease); +int n_dhcp4_client_lease_decline(NDhcp4ClientLease *lease, const char *error); + +/* server configs */ + +int n_dhcp4_server_config_new(NDhcp4ServerConfig **configp); +NDhcp4ServerConfig *n_dhcp4_server_config_free(NDhcp4ServerConfig *config); + +void n_dhcp4_server_config_set_ifindex(NDhcp4ServerConfig *config, int ifindex); + +/* servers */ + +int n_dhcp4_server_new(NDhcp4Server **serverp, NDhcp4ServerConfig *config); +NDhcp4Server *n_dhcp4_server_ref(NDhcp4Server *server); +NDhcp4Server *n_dhcp4_server_unref(NDhcp4Server *server); + +void n_dhcp4_server_get_fd(NDhcp4Server *server, int *fdp); +int n_dhcp4_server_dispatch(NDhcp4Server *server); +int n_dhcp4_server_pop_event(NDhcp4Server *server, NDhcp4ServerEvent **eventp); + +int n_dhcp4_server_add_ip(NDhcp4Server *server, NDhcp4ServerIp **ipp, struct in_addr ip); + +/* server ip addresses */ + +NDhcp4ServerIp *n_dhcp4_server_ip_free(NDhcp4ServerIp *ip); + +/* server leases */ + +NDhcp4ServerLease *n_dhcp4_server_lease_ref(NDhcp4ServerLease *lease); +NDhcp4ServerLease *n_dhcp4_server_lease_unref(NDhcp4ServerLease *lease); + +int n_dhcp4_server_lease_query(NDhcp4ServerLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap); +int n_dhcp4_server_lease_append(NDhcp4ServerLease *lease, uint8_t option, uint8_t *data, size_t n_data); + +int n_dhcp4_server_lease_offer(NDhcp4ServerLease *lease); +int n_dhcp4_server_lease_ack(NDhcp4ServerLease *lease); +int n_dhcp4_server_lease_nack(NDhcp4ServerLease *lease); + +/* inline helpers */ + +static inline void n_dhcp4_client_config_freep(NDhcp4ClientConfig **p) { + if (*p) + n_dhcp4_client_config_free(*p); +} + +static inline void n_dhcp4_client_config_freev(NDhcp4ClientConfig *p) { + n_dhcp4_client_config_free(p); +} + +static inline void n_dhcp4_client_probe_config_freep(NDhcp4ClientProbeConfig **p) { + if (*p) + n_dhcp4_client_probe_config_free(*p); +} + +static inline void n_dhcp4_client_probe_config_freev(NDhcp4ClientProbeConfig *p) { + n_dhcp4_client_probe_config_free(p); +} + +static inline void n_dhcp4_client_unrefp(NDhcp4Client **p) { + if (*p) + n_dhcp4_client_unref(*p); +} + +static inline void n_dhcp4_client_unrefv(NDhcp4Client *p) { + n_dhcp4_client_unref(p); +} + +static inline void n_dhcp4_client_probe_freep(NDhcp4ClientProbe **p) { + if (*p) + n_dhcp4_client_probe_free(*p); +} + +static inline void n_dhcp4_client_probe_freev(NDhcp4ClientProbe *p) { + n_dhcp4_client_probe_free(p); +} + +static inline void n_dhcp4_client_lease_unrefp(NDhcp4ClientLease **p) { + if (*p) + n_dhcp4_client_lease_unref(*p); +} + +static inline void n_dhcp4_client_lease_unrefv(NDhcp4ClientLease *p) { + n_dhcp4_client_lease_unref(p); +} + +static inline void n_dhcp4_server_config_freep(NDhcp4ServerConfig **p) { + if (*p) + n_dhcp4_server_config_free(*p); +} + +static inline void n_dhcp4_server_config_freev(NDhcp4ServerConfig *p) { + n_dhcp4_server_config_free(p); +} + +static inline void n_dhcp4_server_unrefp(NDhcp4Server **p) { + if (*p) + n_dhcp4_server_unref(*p); +} + +static inline void n_dhcp4_server_unrefv(NDhcp4Server *p) { + n_dhcp4_server_unref(p); +} + +static inline void n_dhcp4_server_ip_freep(NDhcp4ServerIp **p) { + if (*p) + n_dhcp4_server_ip_free(*p); +} + +static inline void n_dhcp4_server_ip_freev(NDhcp4ServerIp *p) { + n_dhcp4_server_ip_free(p); +} + +static inline void n_dhcp4_server_lease_unrefp(NDhcp4ServerLease **p) { + if (*p) + n_dhcp4_server_lease_unref(*p); +} + +static inline void n_dhcp4_server_lease_unrefv(NDhcp4ServerLease *p) { + n_dhcp4_server_lease_unref(p); +} + +#ifdef __cplusplus +} +#endif diff --git a/shared/n-dhcp4/src/test-api.c b/shared/n-dhcp4/src/test-api.c new file mode 100644 index 000000000..9e7b6bcaa --- /dev/null +++ b/shared/n-dhcp4/src/test-api.c @@ -0,0 +1,154 @@ +/* + * API Visibility Tests + * This verifies the visibility and availability of the exported API. + */ + +#undef NDEBUG +#include +#include +#include "n-dhcp4.h" + +static void test_api_constants(void) { + assert(1 + N_DHCP4_CLIENT_START_DELAY_RFC2131); + + assert(1 + _N_DHCP4_E_SUCCESS); + assert(1 + N_DHCP4_E_PREEMPTED); + assert(1 + N_DHCP4_E_INTERNAL); + assert(1 + N_DHCP4_E_INVALID_IFINDEX); + assert(1 + N_DHCP4_E_INVALID_TRANSPORT); + assert(1 + N_DHCP4_E_INVALID_ADDRESS); + assert(1 + N_DHCP4_E_INVALID_CLIENT_ID); + assert(1 + N_DHCP4_E_DUPLICATE_OPTION); + assert(1 + N_DHCP4_E_UNSET); + assert(1 + _N_DHCP4_E_N); + + assert(1 + N_DHCP4_TRANSPORT_ETHERNET); + assert(1 + N_DHCP4_TRANSPORT_INFINIBAND); + assert(1 + _N_DHCP4_TRANSPORT_N); + + assert(1 + N_DHCP4_CLIENT_EVENT_DOWN); + assert(1 + N_DHCP4_CLIENT_EVENT_OFFER); + assert(1 + N_DHCP4_CLIENT_EVENT_GRANTED); + assert(1 + N_DHCP4_CLIENT_EVENT_RETRACTED); + assert(1 + N_DHCP4_CLIENT_EVENT_EXTENDED); + assert(1 + N_DHCP4_CLIENT_EVENT_EXPIRED); + assert(1 + N_DHCP4_CLIENT_EVENT_CANCELLED); + assert(1 + _N_DHCP4_CLIENT_EVENT_N); + + assert(1 + N_DHCP4_SERVER_EVENT_DOWN); + assert(1 + N_DHCP4_SERVER_EVENT_DISCOVER); + assert(1 + N_DHCP4_SERVER_EVENT_REQUEST); + assert(1 + N_DHCP4_SERVER_EVENT_RENEW); + assert(1 + N_DHCP4_SERVER_EVENT_DECLINE); + assert(1 + N_DHCP4_SERVER_EVENT_RELEASE); + assert(1 + _N_DHCP4_SERVER_EVENT_N); +} + +static void test_api_types(void) { + assert(sizeof(NDhcp4ClientConfig*) > 0); + assert(sizeof(NDhcp4ClientProbeConfig*) > 0); + assert(sizeof(NDhcp4Client*) > 0); + assert(sizeof(NDhcp4ClientEvent) > 0); + assert(sizeof(NDhcp4ClientProbe*) > 0); + assert(sizeof(NDhcp4ClientLease*) > 0); + assert(sizeof(NDhcp4Server*) > 0); + assert(sizeof(NDhcp4ServerConfig*) > 0); + assert(sizeof(NDhcp4ServerEvent) > 0); + assert(sizeof(NDhcp4ServerIp*) > 0); + assert(sizeof(NDhcp4ServerLease*) > 0); +} + +static void test_api_functions(void) { + void *fns[] = { + (void *)n_dhcp4_client_config_new, + (void *)n_dhcp4_client_config_free, + (void *)n_dhcp4_client_config_freep, + (void *)n_dhcp4_client_config_freev, + (void *)n_dhcp4_client_config_set_ifindex, + (void *)n_dhcp4_client_config_set_transport, + (void *)n_dhcp4_client_config_set_request_broadcast, + (void *)n_dhcp4_client_config_set_mac, + (void *)n_dhcp4_client_config_set_broadcast_mac, + (void *)n_dhcp4_client_config_set_client_id, + + (void *)n_dhcp4_client_probe_config_new, + (void *)n_dhcp4_client_probe_config_free, + (void *)n_dhcp4_client_probe_config_freep, + (void *)n_dhcp4_client_probe_config_freev, + (void *)n_dhcp4_client_probe_config_set_inform_only, + (void *)n_dhcp4_client_probe_config_set_init_reboot, + (void *)n_dhcp4_client_probe_config_set_requested_ip, + (void *)n_dhcp4_client_probe_config_set_start_delay, + (void *)n_dhcp4_client_probe_config_request_option, + (void *)n_dhcp4_client_probe_config_append_option, + + (void *)n_dhcp4_client_new, + (void *)n_dhcp4_client_ref, + (void *)n_dhcp4_client_unref, + (void *)n_dhcp4_client_unrefp, + (void *)n_dhcp4_client_unrefv, + (void *)n_dhcp4_client_get_fd, + (void *)n_dhcp4_client_dispatch, + (void *)n_dhcp4_client_pop_event, + (void *)n_dhcp4_client_update_mtu, + (void *)n_dhcp4_client_probe, + + (void *)n_dhcp4_client_probe_free, + (void *)n_dhcp4_client_probe_freep, + (void *)n_dhcp4_client_probe_freev, + (void *)n_dhcp4_client_probe_get_userdata, + (void *)n_dhcp4_client_probe_set_userdata, + + (void *)n_dhcp4_client_lease_ref, + (void *)n_dhcp4_client_lease_unref, + (void *)n_dhcp4_client_lease_unrefp, + (void *)n_dhcp4_client_lease_unrefv, + (void *)n_dhcp4_client_lease_get_yiaddr, + (void *)n_dhcp4_client_lease_get_lifetime, + (void *)n_dhcp4_client_lease_query, + (void *)n_dhcp4_client_lease_select, + (void *)n_dhcp4_client_lease_accept, + (void *)n_dhcp4_client_lease_decline, + + (void *)n_dhcp4_server_config_new, + (void *)n_dhcp4_server_config_free, + (void *)n_dhcp4_server_config_freep, + (void *)n_dhcp4_server_config_freev, + (void *)n_dhcp4_server_config_set_ifindex, + + (void *)n_dhcp4_server_new, + (void *)n_dhcp4_server_ref, + (void *)n_dhcp4_server_unref, + (void *)n_dhcp4_server_unrefp, + (void *)n_dhcp4_server_unrefv, + (void *)n_dhcp4_server_get_fd, + (void *)n_dhcp4_server_dispatch, + (void *)n_dhcp4_server_pop_event, + (void *)n_dhcp4_server_add_ip, + + (void *)n_dhcp4_server_ip_free, + (void *)n_dhcp4_server_ip_freep, + (void *)n_dhcp4_server_ip_freev, + + (void *)n_dhcp4_server_lease_ref, + (void *)n_dhcp4_server_lease_unref, + (void *)n_dhcp4_server_lease_unrefp, + (void *)n_dhcp4_server_lease_unrefv, + (void *)n_dhcp4_server_lease_query, + (void *)n_dhcp4_server_lease_append, + (void *)n_dhcp4_server_lease_offer, + (void *)n_dhcp4_server_lease_ack, + (void *)n_dhcp4_server_lease_nack, + }; + size_t i; + + for (i = 0; i < sizeof(fns) / sizeof(*fns); ++i) + assert(!!fns[i]); +} + +int main(int argc, char **argv) { + test_api_constants(); + test_api_types(); + test_api_functions(); + return 0; +} diff --git a/shared/n-dhcp4/src/test-connection.c b/shared/n-dhcp4/src/test-connection.c new file mode 100644 index 000000000..360076e7b --- /dev/null +++ b/shared/n-dhcp4/src/test-connection.c @@ -0,0 +1,388 @@ +/* + * Tests for DHCP4 Client Connections + */ + +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4-private.h" +#include "test.h" +#include "util/link.h" +#include "util/netns.h" +#include "util/packet.h" + +static void test_poll_client(int efd, unsigned int u32) { + struct epoll_event event = {}; + int r; + + r = epoll_wait(efd, &event, 1, -1); + c_assert(r == 1); + c_assert(event.events == EPOLLIN); + c_assert(event.data.u32 == u32); +} + +static void test_poll_server(int fd) { + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + int r; + + r = poll(&pfd, 1, -1); + c_assert(r == 1); + c_assert(pfd.revents == POLLIN); +} + +static void test_s_connection_init(int netns, NDhcp4SConnection *connection, int ifindex) { + int r, oldns; + + netns_get(&oldns); + netns_set(netns); + + r = n_dhcp4_s_connection_init(connection, ifindex); + c_assert(!r); + + netns_set(oldns); +} + +static void test_c_connection_listen(int netns, NDhcp4CConnection *connection) { + int r, oldns; + + netns_get(&oldns); + netns_set(netns); + + r = n_dhcp4_c_connection_listen(connection); + c_assert(!r); + + netns_set(oldns); +} + +static void test_c_connection_connect(int netns, + NDhcp4CConnection *connection, + const struct in_addr *client, + const struct in_addr *server) { + int r, oldns; + + netns_get(&oldns); + netns_set(netns); + + r = n_dhcp4_c_connection_connect(connection, client, server); + c_assert(!r); + + netns_set(oldns); +} + +static void test_server_receive(NDhcp4SConnection *connection, uint8_t expected_type, NDhcp4Incoming **messagep) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + uint8_t received_type; + int r, fd; + + n_dhcp4_s_connection_get_fd(connection, &fd); + test_poll_server(fd); + + r = n_dhcp4_s_connection_dispatch_io(connection, &message); + c_assert(!r); + c_assert(message); + + r = n_dhcp4_incoming_query_message_type(message, &received_type); + c_assert(!r); + c_assert(received_type == expected_type); + + if (messagep) { + *messagep = message; + message = NULL; + } +} + +static void test_client_receive(NDhcp4CConnection *connection, uint8_t expected_type, NDhcp4Incoming **messagep) { + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL; + uint8_t received_type; + int r; + + test_poll_client(connection->fd_epoll, N_DHCP4_CLIENT_EPOLL_IO); + + r = n_dhcp4_c_connection_dispatch_io(connection, &message); + c_assert(!r); + c_assert(message); + + r = n_dhcp4_incoming_query_message_type(message, &received_type); + c_assert(!r); + c_assert(received_type == expected_type); + + if (messagep) { + *messagep = message; + message = NULL; + } +} + +static void test_discover(NDhcp4SConnection *connection_server, + NDhcp4CConnection *connection_client, + const struct in_addr *addr_server, + const struct in_addr *addr_client, + NDhcp4Incoming **offerp) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL; + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply_out = NULL; + int r; + + r = n_dhcp4_c_connection_discover_new(connection_client, &request_out); + c_assert(!r); + + r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0); + c_assert(!r); + request_out = NULL; + + test_server_receive(connection_server, N_DHCP4_MESSAGE_DISCOVER, &request_in); + + r = n_dhcp4_s_connection_offer_new(connection_server, &reply_out, request_in, addr_server, addr_client, 60); + c_assert(!r); + + r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply_out); + c_assert(!r); + + test_client_receive(connection_client, N_DHCP4_MESSAGE_OFFER, offerp); +} + +static void test_select(NDhcp4SConnection *connection_server, + NDhcp4CConnection *connection_client, + NDhcp4Incoming *offer, + const struct in_addr *addr_server, + const struct in_addr *addr_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL; + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL; + int r; + + r = n_dhcp4_c_connection_select_new(connection_client, &request_out, offer); + c_assert(!r); + + r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0); + c_assert(!r); + request_out = NULL; + + test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in); + + r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60); + c_assert(!r); + + r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply); + c_assert(!r); + + test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, NULL); +} + +static void test_reboot(NDhcp4SConnection *connection_server, + NDhcp4CConnection *connection_client, + const struct in_addr *addr_server, + const struct in_addr *addr_client, + NDhcp4Incoming **ackp) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL; + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL; + int r; + + r = n_dhcp4_c_connection_reboot_new(connection_client, &request_out, addr_server); + c_assert(!r); + + r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0); + c_assert(!r); + request_out = NULL; + + test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in); + + r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60); + c_assert(!r); + + r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply); + c_assert(!r); + + test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, ackp); +} + +static void test_decline(NDhcp4SConnection *connection_server, + NDhcp4CConnection *connection_client, + NDhcp4Incoming *ack) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL; + int r; + + r = n_dhcp4_c_connection_decline_new(connection_client, &request_out, ack, "No thanks."); + c_assert(!r); + + r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0); + c_assert(!r); + request_out = NULL; + + test_server_receive(connection_server, N_DHCP4_MESSAGE_DECLINE, NULL); +} + + +static void test_renew(NDhcp4SConnection *connection_server, + NDhcp4CConnection *connection_client, + const struct in_addr *addr_server, + const struct in_addr *addr_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL; + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL; + int r; + + r = n_dhcp4_c_connection_renew_new(connection_client, &request_out); + c_assert(!r); + + r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0); + c_assert(!r); + request_out = NULL; + + test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in); + + r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60); + c_assert(!r); + + r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply); + c_assert(!r); + + test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, NULL); +} + +static void test_rebind(NDhcp4SConnection *connection_server, + NDhcp4CConnection *connection_client, + const struct in_addr *addr_server, + const struct in_addr *addr_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL; + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL; + int r; + + r = n_dhcp4_c_connection_rebind_new(connection_client, &request_out); + c_assert(!r); + + r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0); + c_assert(!r); + request_out = NULL; + + test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in); + + r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60); + c_assert(!r); + + r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply); + c_assert(!r); + + test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, NULL); +} + +static void test_release(NDhcp4SConnection *connection_server, + NDhcp4CConnection *connection_client, + const struct in_addr *addr_server, + const struct in_addr *addr_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL; + int r; + + r = n_dhcp4_c_connection_release_new(connection_client, &request_out, "Shutting down!"); + c_assert(!r); + + r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0); + c_assert(!r); + request_out = NULL; + + test_server_receive(connection_server, N_DHCP4_MESSAGE_RELEASE, NULL); +} + +static void test_connection(void) { + const struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) }; + const struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) }; + _c_cleanup_(netns_closep) int ns_server = -1, ns_client = -1; + _c_cleanup_(link_deinit) Link link_server = LINK_NULL(link_server); + _c_cleanup_(link_deinit) Link link_client = LINK_NULL(link_client); + _c_cleanup_(c_closep) int efd_client = -1; + int r; + + /* setup */ + + netns_new(&ns_server); + netns_new(&ns_client); + + link_new_veth(&link_server, &link_client, ns_server, ns_client); + link_add_ip4(&link_server, &addr_server, 8); + + efd_client = epoll_create1(EPOLL_CLOEXEC); + c_assert(efd_client >= 0); + + /* test connections */ + { + _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *client_config = NULL; + _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *probe_config = NULL; + NDhcp4SConnection connection_server = N_DHCP4_S_CONNECTION_NULL(connection_server); + NDhcp4SConnectionIp connection_server_ip = N_DHCP4_S_CONNECTION_IP_NULL(connection_server_ip); + NDhcp4CConnection connection_client = N_DHCP4_C_CONNECTION_NULL(connection_client); + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *offer = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *ack = NULL; + + test_s_connection_init(ns_server, &connection_server, link_server.ifindex); + n_dhcp4_s_connection_ip_init(&connection_server_ip, addr_server); + n_dhcp4_s_connection_ip_link(&connection_server_ip, &connection_server); + + r = n_dhcp4_client_config_new(&client_config); + c_assert(!r); + + n_dhcp4_client_config_set_ifindex(client_config, link_client.ifindex); + n_dhcp4_client_config_set_transport(client_config, N_DHCP4_TRANSPORT_ETHERNET); + n_dhcp4_client_config_set_request_broadcast(client_config, false); + n_dhcp4_client_config_set_mac(client_config, link_client.mac.ether_addr_octet, ETH_ALEN); + n_dhcp4_client_config_set_broadcast_mac(client_config, + (const uint8_t[]){ + 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, + }, + ETH_ALEN); + r = n_dhcp4_client_config_set_client_id(client_config, + (void *)"client-id", + strlen("client-id")); + c_assert(!r); + + r = n_dhcp4_client_probe_config_new(&probe_config); + c_assert(!r); + + r = n_dhcp4_c_connection_init(&connection_client, + client_config, + probe_config, + efd_client); + c_assert(!r); + test_c_connection_listen(ns_client, &connection_client); + + test_discover(&connection_server, &connection_client, &addr_server, &addr_client, &offer); + test_select(&connection_server, &connection_client, offer, &addr_server, &addr_client); + test_reboot(&connection_server, &connection_client, &addr_server, &addr_client, &ack); + test_decline(&connection_server, &connection_client, ack); + + link_add_ip4(&link_client, &addr_client, 8); + test_c_connection_connect(ns_client, &connection_client, &addr_client, &addr_server); + + test_renew(&connection_server, &connection_client, &addr_server, &addr_client); + test_rebind(&connection_server, &connection_client, &addr_server, &addr_client); + test_release(&connection_server, &connection_client, &addr_server, &addr_client); + + n_dhcp4_c_connection_deinit(&connection_client); + n_dhcp4_s_connection_ip_unlink(&connection_server_ip); + n_dhcp4_s_connection_ip_deinit(&connection_server_ip); + n_dhcp4_s_connection_deinit(&connection_server); + } + + /* teardown */ + + link_del_ip4(&link_client, &addr_client, 8); + link_del_ip4(&link_server, &addr_server, 8); +} + +int main(int argc, char **argv) { + test_setup(); + + test_connection(); + + return 0; +} diff --git a/shared/n-dhcp4/src/test-message.c b/shared/n-dhcp4/src/test-message.c new file mode 100644 index 000000000..6d4200f78 --- /dev/null +++ b/shared/n-dhcp4/src/test-message.c @@ -0,0 +1,159 @@ +/* + * Tests for DHCP4 Message Handling + */ + +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4-private.h" + +static void test_outgoing(void) { + NDhcp4Outgoing *outgoing; + int r; + + /* verify basic NEW/FREE */ + + outgoing = NULL; + r = n_dhcp4_outgoing_new(&outgoing, 0, 0); + c_assert(!r); + c_assert(outgoing); + + outgoing = n_dhcp4_outgoing_free(outgoing); + c_assert(!outgoing); +} + +static void test_incoming(void) { + NDhcp4Incoming *incoming; + struct { + NDhcp4Header header; + uint8_t sname[64]; + uint8_t file[128]; + uint32_t magic; + uint8_t options[1024]; + } m; + uint8_t *v; + size_t l; + int r; + + /* verify that messages must be at least the size of the header */ + + r = n_dhcp4_incoming_new(&incoming, NULL, 0); + c_assert(r == N_DHCP4_E_MALFORMED); + + r = n_dhcp4_incoming_new(&incoming, NULL, sizeof(m.header) + 64 + 128 + 3); + c_assert(r == N_DHCP4_E_MALFORMED); + + /* verify that magic must be set */ + + memset(&m, 0, sizeof(m)); + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(r == N_DHCP4_E_MALFORMED); + + /* verify basic NEW/FREE */ + + memset(&m, 0, sizeof(m)); + m.magic = htobe32(N_DHCP4_MESSAGE_MAGIC); + incoming = NULL; + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m.header) + 64 + 128 + 4); + c_assert(!r); + c_assert(incoming); + + incoming = n_dhcp4_incoming_free(incoming); + c_assert(!incoming); + + /* verify that PAD is properly handled */ + + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(!r); + incoming = n_dhcp4_incoming_free(incoming); + + /* verify that SNAME/FILE are only looked at if OVERLOAD is set */ + + m.sname[0] = 1; + m.sname[1] = 0; + m.file[0] = 2; + m.file[1] = 0; + + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(!r); + r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL); + c_assert(r == N_DHCP4_E_UNSET); + r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL); + c_assert(r == N_DHCP4_E_UNSET); + incoming = n_dhcp4_incoming_free(incoming); + + m.options[0] = N_DHCP4_OPTION_OVERLOAD; + m.options[1] = 1; + m.options[2] = 0; + + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(!r); + r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL); + c_assert(r == N_DHCP4_E_UNSET); + r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL); + c_assert(r == N_DHCP4_E_UNSET); + incoming = n_dhcp4_incoming_free(incoming); + + m.options[0] = N_DHCP4_OPTION_OVERLOAD; + m.options[1] = 1; + m.options[2] = N_DHCP4_OVERLOAD_SNAME; + + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(!r); + r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL); + c_assert(r == 0); + r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL); + c_assert(r == N_DHCP4_E_UNSET); + incoming = n_dhcp4_incoming_free(incoming); + + m.options[0] = N_DHCP4_OPTION_OVERLOAD; + m.options[1] = 1; + m.options[2] = N_DHCP4_OVERLOAD_FILE; + + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(!r); + r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL); + c_assert(r == N_DHCP4_E_UNSET); + r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL); + c_assert(r == 0); + incoming = n_dhcp4_incoming_free(incoming); + + m.options[0] = N_DHCP4_OPTION_OVERLOAD; + m.options[1] = 1; + m.options[2] = N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME; + + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(!r); + r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL); + c_assert(r == 0); + r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL); + c_assert(r == 0); + incoming = n_dhcp4_incoming_free(incoming); + + /* verify basic concatenation */ + + m.options[3] = 1; + m.options[4] = 1; + m.options[5] = 0xef; + m.sname[1] = 1; + m.sname[2] = 0xcf; + + r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m)); + c_assert(!r); + r = n_dhcp4_incoming_query(incoming, 1, &v, &l); + c_assert(r == 0); + c_assert(l == 2); + c_assert(v[0] == 0xef && v[1] == 0xcf); + incoming = n_dhcp4_incoming_free(incoming); +} + +int main(int argc, char **argv) { + test_outgoing(); + test_incoming(); + return 0; +} diff --git a/shared/n-dhcp4/src/test-run-client.c b/shared/n-dhcp4/src/test-run-client.c new file mode 100644 index 000000000..d29dbf1f2 --- /dev/null +++ b/shared/n-dhcp4/src/test-run-client.c @@ -0,0 +1,713 @@ +/* + * DHCP Client Runner + * + * This test implements a DHCP client. It takes parameters via the command-line + * and runs a DHCP client. It is mainly meant for testing, as such it allows + * tweaking that an exported DHCP client should not provide. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4.h" +#include "n-dhcp4-private.h" +#include "test.h" + +typedef struct Manager Manager; + +enum { + _MAIN_SUCCESS, + MAIN_EXIT, + MAIN_FAILED, +}; + +struct Manager { + NDhcp4Client *client; + NDhcp4ClientProbe *probe; +}; + +#define MANAGER_NULL(_x) {} + +static struct ether_addr main_arg_broadcast_mac = {}; +static bool main_arg_broadcast_mac_set = false; +static uint8_t* main_arg_client_id = NULL; +static size_t main_arg_n_client_id = 0; +static int main_arg_ifindex = 0; +static struct in_addr main_arg_requested_ip = { INADDR_ANY }; +static long long int main_arg_requested_lifetime = -1; +static uint8_t main_arg_requested_parameters[UINT8_MAX] = {}; +static size_t main_arg_n_requested_parameters = 0; +static struct ether_addr main_arg_mac = {}; +static bool main_arg_mac_set = false; +static bool main_arg_request_broadcast = false; +static bool main_arg_test = false; + +static Manager *manager_free(Manager *manager) { + if (!manager) + return NULL; + + n_dhcp4_client_probe_free(manager->probe); + n_dhcp4_client_unref(manager->client); + free(manager); + + return NULL; +} + +static void manager_freep(Manager **manager) { + manager_free(*manager); +} + +static int manager_new(Manager **managerp) { + _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL; + _c_cleanup_(manager_freep) Manager *manager = NULL; + int r; + + manager = malloc(sizeof(*manager)); + if (!manager) + return -ENOMEM; + + *manager = (Manager)MANAGER_NULL(*manager); + + r = n_dhcp4_client_config_new(&config); + if (r) + return r; + + n_dhcp4_client_config_set_broadcast_mac(config, + &main_arg_broadcast_mac.ether_addr_octet[0], + sizeof(main_arg_broadcast_mac.ether_addr_octet)); + n_dhcp4_client_config_set_mac(config, + &main_arg_mac.ether_addr_octet[0], + sizeof(main_arg_mac.ether_addr_octet)); + n_dhcp4_client_config_set_client_id(config, + main_arg_client_id, + main_arg_n_client_id); + n_dhcp4_client_config_set_ifindex(config, main_arg_ifindex); + n_dhcp4_client_config_set_request_broadcast(config, main_arg_request_broadcast); + n_dhcp4_client_config_set_transport(config, N_DHCP4_TRANSPORT_ETHERNET); + + r = n_dhcp4_client_new(&manager->client, config); + if (r) + return r; + + *managerp = manager; + manager = NULL; + return 0; +} + +static int manager_lease_get_dns(NDhcp4ClientLease *lease, struct in_addr *dns) { + uint8_t *data; + size_t n_data; + int r; + + r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_DOMAIN_NAME_SERVER, &data, &n_data); + if (r) + return r; + + if (n_data < sizeof(dns->s_addr)) + return N_DHCP4_E_MALFORMED; + + memcpy(&dns->s_addr, data, sizeof(dns->s_addr)); + + return 0; +} + +static int manager_lease_get_router(NDhcp4ClientLease *lease, struct in_addr *router) { + uint8_t *data; + size_t n_data; + int r; + + r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_ROUTER, &data, &n_data); + if (r) + return r; + + if (n_data < sizeof(router->s_addr)) + return N_DHCP4_E_MALFORMED; + + memcpy(&router->s_addr, data, sizeof(router->s_addr)); + + return 0; +} + +static int manager_lease_get_subnetmask(NDhcp4ClientLease *lease, struct in_addr *mask) { + uint8_t *data; + size_t n_data; + int r; + + r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_SUBNET_MASK, &data, &n_data); + if (r) + return r; + + if (n_data != sizeof(mask->s_addr)) + return N_DHCP4_E_MALFORMED; + + memcpy(&mask->s_addr, data, sizeof(mask->s_addr)); + + return 0; +} + +static int manager_lease_get_prefix(NDhcp4ClientLease *lease, unsigned int *prefixp) { + struct in_addr mask = {}; + unsigned int postfix; + int r; + + r = manager_lease_get_subnetmask(lease, &mask); + if (r) + return r; + + postfix =__builtin_ctz(ntohl(mask.s_addr)); + c_assert(postfix <= 32); + + if (postfix < 32) { + if ((~ntohl(mask.s_addr)) >> postfix != 0) + return N_DHCP4_E_MALFORMED; + } + + *prefixp = 32 - postfix; + return 0; +} + +static int manager_check(Manager *manager, NDhcp4ClientLease *lease) { + int r; + + r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_ROUTER, NULL, NULL); + if (r) { + fprintf(stderr, "No router\n"); + return r; + } + + r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_SUBNET_MASK, NULL, NULL); + if (r) { + fprintf(stderr, "No subnet mask\n"); + return r; + } + + return r; +} + +static int manager_add(Manager *manager, NDhcp4ClientLease *lease) { + char *p, ifname[IF_NAMESIZE + 1] = {}; + struct in_addr router = {}, yiaddr = {}, dns = {}; + unsigned int prefix; + uint64_t lifetime; + int r; + + n_dhcp4_client_lease_get_yiaddr(lease, &yiaddr); + n_dhcp4_client_lease_get_lifetime(lease, &lifetime); + + r = manager_lease_get_router(lease, &router); + if (r) + return r; + + r = manager_lease_get_prefix(lease, &prefix); + if (r) + return r; + + p = if_indextoname(main_arg_ifindex, ifname); + c_assert(p); + + if (lifetime == UINT64_MAX) { + r = asprintf(&p, "ip addr add %s/%u dev %s preferred_lft forever valid_lft forever", inet_ntoa(yiaddr), prefix, ifname); + c_assert(r >= 0); + } else { + r = asprintf(&p, "ip addr add %s/%u dev %s preferred_lft %llu valid_lft %llu", inet_ntoa(yiaddr), prefix, ifname, lifetime / 1000000000ULL, lifetime / 1000000000ULL); + c_assert(r >= 0); + } + r = system(p); + c_assert(r == 0); + free(p); + + r = asprintf(&p, "ip route add %s/32 dev %s", inet_ntoa(router), ifname); + c_assert(r >= 0); + r = system(p); + c_assert(r == 0); + free(p); + + r = asprintf(&p, "ip route add default via %s dev %s", inet_ntoa(router), ifname); + c_assert(r >= 0); + r = system(p); + c_assert(r == 0); + free(p); + + r = manager_lease_get_dns(lease, &dns); + if (r) { + if (r != N_DHCP4_E_UNSET) + return r; + } else { + fprintf(stderr, "DNS: %s\n", inet_ntoa(dns)); + } + + return 0; +} + +static int manager_dispatch(Manager *manager) { + NDhcp4ClientEvent *event; + int r; + + r = n_dhcp4_client_dispatch(manager->client); + if (r) { + if (r != N_DHCP4_E_PREEMPTED) { + /* + * We are level-triggered, so we do not need to react + * to preemption. We simply continue the mainloop. + */ + return r; + } + } + + for (;;) { + r = n_dhcp4_client_pop_event(manager->client, &event); + if (r) + return r; + + if (!event) + break; + + switch (event->event) { + case N_DHCP4_CLIENT_EVENT_DOWN: + fprintf(stderr, "DOWN\n"); + + break; + + case N_DHCP4_CLIENT_EVENT_OFFER: + fprintf(stderr, "OFFER\n"); + + r = manager_check(manager, event->granted.lease); + if (r) { + if (r == N_DHCP4_E_UNSET) { + fprintf(stderr, "Missing mandatory option, ignoring lease.\n"); + } else { + return r; + } + } else { + r = n_dhcp4_client_lease_select(event->offer.lease); + if (r) + return r; + } + + break; + + case N_DHCP4_CLIENT_EVENT_GRANTED: + fprintf(stderr, "GRANTED\n"); + + r = manager_add(manager, event->granted.lease); + if (r) { + if (r == N_DHCP4_E_UNSET) { + fprintf(stderr, "Missing mandatory option, declining lease.\n"); + + r = n_dhcp4_client_lease_decline(event->granted.lease, "Missing mandatory option."); + if (r) + return r; + } else { + return r; + } + } else { + r = n_dhcp4_client_lease_accept(event->granted.lease); + if (r) + return r; + } + + break; + + case N_DHCP4_CLIENT_EVENT_RETRACTED: + fprintf(stderr, "RETRACTED\n"); + + break; + + case N_DHCP4_CLIENT_EVENT_EXTENDED: + fprintf(stderr, "EXTENDED\n"); + + break; + + case N_DHCP4_CLIENT_EVENT_EXPIRED: + fprintf(stderr, "EXPIRED\n"); + + break; + + case N_DHCP4_CLIENT_EVENT_CANCELLED: + fprintf(stderr, "CANCELLED\n"); + + break; + + default: + fprintf(stderr, "Unexpected event: %u\n", event->event); + + break; + } + } + + return 0; +} + +static int manager_run(Manager *manager) { + _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL; + int r; + + r = n_dhcp4_client_probe_config_new(&config); + if (r) + return r; + + /* + * Let's speed up our tests, while still making sure the code-path + * for the deferrment is actually tested (so don't set it to zero). + */ + n_dhcp4_client_probe_config_set_start_delay(config, 10); + + n_dhcp4_client_probe_config_set_requested_ip(config, main_arg_requested_ip); + + if (main_arg_n_requested_parameters > 0) { + for (unsigned int i = 0; i < main_arg_n_requested_parameters; ++i) + n_dhcp4_client_probe_config_request_option(config, main_arg_requested_parameters[i]); + } else { + n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_ROUTER); + n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_SUBNET_MASK); + n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_DOMAIN_NAME_SERVER); + } + + if (main_arg_requested_lifetime >= 0) { + uint32_t lifetime = ntohl(main_arg_requested_lifetime); + + r = n_dhcp4_client_probe_config_append_option(config, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, &lifetime, sizeof(lifetime)); + if (r) + return r; + } + + r = n_dhcp4_client_probe(manager->client, &manager->probe, config); + if (r) + return r; + + /* + * The test-suite runs this with the --test argument. So far, we do not + * perform any fancy runtime tests, but simply exit the main-loop + * immediately. We can add more elaborate tests in the future. + */ + if (main_arg_test) + return 0; + + for (;;) { + struct pollfd pfds[] = { + { .fd = -1, .events = POLLIN }, + }; + size_t i; + int n; + + n_dhcp4_client_get_fd(manager->client, &pfds[0].fd); + + n = poll(pfds, sizeof(pfds) / sizeof(*pfds), -1); + if (n < 0) + return -errno; + + for (i = 0; i < (size_t)n; ++i) { + if (pfds[i].revents & ~POLLIN) + return -ENOTRECOVERABLE; + + if (!(pfds[i].revents & POLLIN)) + continue; + + r = manager_dispatch(manager); + if (r) + return r; + } + } + + return 0; +} + +static int run(void) { + _c_cleanup_(manager_freep) Manager *manager = NULL; + int r; + + r = manager_new(&manager); + if (r) + return r; + + return manager_run(manager); +} + +static void print_help(void) { + printf("%s [GLOBALS...] ...\n\n" + "DHCP Test Client\n\n" + " -h --help Show this help\n" + " --test Run as part of the test suite\n" + " --ifindex IDX Index of interface to run on\n" + " --mac HEX Hardware address to use\n" + " --broadcast-mac HEX Broadcast hardware address to use\n" + " --requested-ip IP Requested IP adress\n" + " --requested-lifetime SECS Requested lease lifetime in seconds\n" + " --requested-parameters P1,P2,... Requested parameters\n" + " --client-id HEX Client Identifier to use\n" + , program_invocation_short_name); +} + +static int setup_test(void) { + test_setup(); + + /* --broadcast-mac */ + { + main_arg_broadcast_mac_set = true; + } + + /* --ifindex */ + { + main_arg_ifindex = 1; + } + + /* --mac */ + { + main_arg_mac_set = true; + } + + return 0; +} + +static int parse_hexstr(const char *in, uint8_t **outp, size_t *n_outp) { + _c_cleanup_(c_freep) uint8_t *out = NULL; + size_t i, n_in, n_out; + + n_in = strlen(in); + n_out = (n_in + 1) / 2; + + out = malloc(n_out); + if (!out) + return -ENOMEM; + + for (i = 0; i < n_in; ++i) { + uint8_t v = 0; + + switch (in[i]) { + case '0'...'9': + v = in[i] - '0'; + break; + case 'a'...'f': + v = in[i] - 'a' + 0xa; + break; + case 'A'...'F': + v = in[i] - 'A' + 0xa; + break; + } + + if (i % 2) { + out[i / 2] <<= 4; + out[i / 2] |= v; + } else { + out[i / 2] = v; + } + } + + *outp = out; + out = NULL; + *n_outp = n_out; + return 0; +} + +static int parse_argv(int argc, char **argv) { + enum { + _ARG_0 = 0x100, + ARG_BROADCAST_MAC, + ARG_CLIENT_ID, + ARG_IFINDEX, + ARG_MAC, + ARG_REQUEST_BROADCAST, + ARG_REQUESTED_IP, + ARG_REQUESTED_LIFETIME, + ARG_REQUESTED_PARAMETERS, + ARG_TEST, + }; + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "broadcast-mac", required_argument, NULL, ARG_BROADCAST_MAC }, + { "client-id", required_argument, NULL, ARG_CLIENT_ID }, + { "ifindex", required_argument, NULL, ARG_IFINDEX }, + { "mac", required_argument, NULL, ARG_MAC }, + { "request-broadcast", no_argument, NULL, ARG_REQUEST_BROADCAST }, + { "requested-ip", required_argument, NULL, ARG_REQUESTED_IP }, + { "requested-lifetime", required_argument, NULL, ARG_REQUESTED_LIFETIME }, + { "requested-parameters", required_argument, NULL, ARG_REQUESTED_PARAMETERS }, + { "test", no_argument, NULL, ARG_TEST }, + {} + }; + struct ether_addr *addr; + long long int lli; + size_t n; + void *t; + int r, c; + + /* + * Most of the argument-parsers are short-and-dirty hacks to make the + * conversions work. This is sufficient for a test-client, but needs + * proper error-checking if done outside of tests. + */ + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) { + switch (c) { + case 'h': + print_help(); + return MAIN_EXIT; + + case ARG_BROADCAST_MAC: + addr = ether_aton_r(optarg, &main_arg_broadcast_mac); + if (!addr) { + fprintf(stderr, + "%s: invalid broadcast mac address -- '%s'\n", + program_invocation_name, + optarg); + return MAIN_FAILED; + } + + main_arg_broadcast_mac_set = true; + break; + + case ARG_CLIENT_ID: + r = parse_hexstr(optarg, (uint8_t **)&t, &n); + if (r) + return r; + + free(main_arg_client_id); + main_arg_client_id = t; + main_arg_n_client_id = n; + break; + + case ARG_IFINDEX: + main_arg_ifindex = atoi(optarg); + break; + + case ARG_MAC: + addr = ether_aton_r(optarg, &main_arg_mac); + if (!addr) { + fprintf(stderr, + "%s: invalid mac address -- '%s'\n", + program_invocation_name, + optarg); + return MAIN_FAILED; + } + + main_arg_mac_set = true; + break; + + case ARG_REQUEST_BROADCAST: + main_arg_request_broadcast = true; + break; + + case ARG_REQUESTED_IP: + r = inet_aton(optarg, &main_arg_requested_ip); + if (r != 1) { + fprintf(stderr, + "%s: invalid requested IP -- '%s'\n", + program_invocation_name, + optarg); + return MAIN_FAILED; + } + break; + + case ARG_REQUESTED_LIFETIME: + lli = atoll(optarg); + if (lli < 0 || lli > UINT32_MAX) { + fprintf(stderr, + "%s: invalid requested lifetime -- '%s'\n", + program_invocation_name, + optarg); + return MAIN_FAILED; + } + main_arg_requested_lifetime = lli; + break; + + case ARG_REQUESTED_PARAMETERS: + for (const char *param = optarg; param; param = strchr(param, ',') ? strchr(param, ',') + 1 : NULL) { + c_assert(main_arg_n_requested_parameters <= UINT8_MAX); + + lli = atoll(param); + if (lli < 0 || lli > UINT8_MAX) { + fprintf(stderr, + "%s: invalid requested parameters -- '%s'\n", + program_invocation_name, + optarg); + return MAIN_FAILED; + } + main_arg_requested_parameters[main_arg_n_requested_parameters++] = lli; + } + break; + + case ARG_TEST: + r = setup_test(); + if (r) + return r; + + main_arg_test = true; + break; + + case '?': + /* getopt_long() prints warning */ + return MAIN_FAILED; + + default: + return -ENOTRECOVERABLE; + } + } + + if (optind != argc) { + fprintf(stderr, + "%s: invalid arguments -- '%s'\n", + program_invocation_name, + argv[optind]); + return MAIN_FAILED; + } + + if (!main_arg_broadcast_mac_set || + !main_arg_ifindex || + !main_arg_mac_set) { + fprintf(stderr, + "%s: required arguments: broadcast-mac, ifindex, mac\n", + program_invocation_name); + return MAIN_FAILED; + } + + return 0; +} + +int main(int argc, char **argv) { + int r; + + /* --client-id */ + { + uint8_t *b; + size_t n; + + n = strlen("client-id"); + b = malloc(n); + c_assert(b); + memcpy(b, "client-id", n); + + free(main_arg_client_id); + main_arg_client_id = b; + main_arg_n_client_id = n; + } + + r = parse_argv(argc, argv); + if (r) + goto exit; + + r = run(); + +exit: + if (r == MAIN_EXIT) { + r = 0; + } else if (r < 0) { + errno = -r; + fprintf(stderr, "Failed with system errno %d: %m\n", r); + r = 127; + } else if (r > 0) { + fprintf(stderr, "Failed with internal error %d\n", r); + } + + free(main_arg_client_id); + + return r; +} diff --git a/shared/n-dhcp4/src/test-socket.c b/shared/n-dhcp4/src/test-socket.c new file mode 100644 index 000000000..7d1756534 --- /dev/null +++ b/shared/n-dhcp4/src/test-socket.c @@ -0,0 +1,314 @@ +/* + * Tests for DHCP4 Socket Helpers + */ + +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "n-dhcp4-private.h" +#include "test.h" +#include "util/link.h" +#include "util/netns.h" +#include "util/packet.h" + +static void test_poll(int sk) { + int r; + + r = poll(&(struct pollfd){ .fd = sk, .events = POLLIN }, 1, -1); + c_assert(r == 1); +} + +static void test_client_packet_socket_new(Link *link, int *skp) { + int r, oldns; + + netns_get(&oldns); + netns_set(link->netns); + + r = n_dhcp4_c_socket_packet_new(skp, link->ifindex); + c_assert(r >= 0); + + netns_set(oldns); +} + +static void test_client_udp_socket_new(Link *link, + int *skp, + const struct in_addr *addr_client, + const struct in_addr *addr_server) { + int r, oldns; + + netns_get(&oldns); + netns_set(link->netns); + + r = n_dhcp4_c_socket_udp_new(skp, link->ifindex, addr_client, addr_server); + c_assert(r >= 0); + + netns_set(oldns); +} + +static void test_server_packet_socket_new(Link *link, int *skp) { + int r, oldns; + + netns_get(&oldns); + netns_set(link->netns); + + r = n_dhcp4_s_socket_packet_new(skp); + c_assert(r >= 0); + + netns_set(oldns); +} + +static void test_server_udp_socket_new(Link *link, int *skp) { + int r, oldns; + + netns_get(&oldns); + netns_set(link->netns); + + r = n_dhcp4_s_socket_udp_new(skp, link->ifindex); + c_assert(r >= 0); + + netns_set(oldns); +} + +static void test_client_server_packet(Link *link_server, Link *link_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL; + _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1; + uint8_t buf[UINT16_MAX]; + struct sockaddr_in dest = {}; + int r; + + test_server_udp_socket_new(link_server, &sk_server); + test_client_packet_socket_new(link_client, &sk_client); + + r = n_dhcp4_outgoing_new(&outgoing, 0, 0); + c_assert(!r); + n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREQUEST; + + r = n_dhcp4_c_socket_packet_send(sk_client, + link_client->ifindex, + (const unsigned char[]){0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + ETH_ALEN, + outgoing); + c_assert(!r); + + test_poll(sk_server); + + r = n_dhcp4_s_socket_udp_recv(sk_server, buf, sizeof(buf), &incoming, &dest); + c_assert(!r); + c_assert(incoming); + c_assert(dest.sin_family == AF_INET); + c_assert(dest.sin_port == htons(N_DHCP4_NETWORK_SERVER_PORT)); + c_assert(dest.sin_addr.s_addr == INADDR_BROADCAST); +} + +static void test_client_server_udp(Link *link_server, Link *link_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL; + _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1; + struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) }; + struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) }; + uint8_t buf[UINT16_MAX]; + struct sockaddr_in dest = {}; + int r; + + /* setup */ + + link_add_ip4(link_server, &addr_server, 8); + link_add_ip4(link_client, &addr_client, 8); + + /* test communication */ + + test_server_udp_socket_new(link_server, &sk_server); + test_client_udp_socket_new(link_client, &sk_client, &addr_client, &addr_server); + + r = n_dhcp4_outgoing_new(&outgoing, 0, 0); + c_assert(!r); + n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREQUEST; + + r = n_dhcp4_c_socket_udp_send(sk_client, outgoing); + c_assert(!r); + + test_poll(sk_server); + + r = n_dhcp4_s_socket_udp_recv(sk_server, buf, sizeof(buf), &incoming, &dest); + c_assert(!r); + c_assert(incoming); + c_assert(dest.sin_family == AF_INET); + c_assert(dest.sin_port == htons(N_DHCP4_NETWORK_SERVER_PORT)); + c_assert(dest.sin_addr.s_addr == addr_server.s_addr); + + /* teardown */ + + link_del_ip4(link_client, &addr_client, 8); + link_del_ip4(link_server, &addr_server, 8); +} + +static void test_server_client_packet(Link *link_server, Link *link_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming1 = NULL, *incoming2 = NULL; + _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1; + struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) }; + struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) }; + uint8_t buf[UINT16_MAX]; + int r; + + /* setup */ + + link_add_ip4(link_server, &addr_server, 8); + + /* test communication */ + + test_server_packet_socket_new(link_server, &sk_server); + test_client_packet_socket_new(link_client, &sk_client); + + r = n_dhcp4_outgoing_new(&outgoing, 0, 0); + c_assert(!r); + n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREPLY; + + r = n_dhcp4_s_socket_packet_send(sk_server, + link_server->ifindex, + &addr_server, + link_client->mac.ether_addr_octet, + ETH_ALEN, + &addr_client, + outgoing); + c_assert(!r); + r = n_dhcp4_s_socket_packet_send(sk_server, + link_server->ifindex, + &addr_server, + (const unsigned char[]){ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + }, + ETH_ALEN, + &addr_client, + outgoing); + c_assert(!r); + + test_poll(sk_client); + + r = n_dhcp4_c_socket_packet_recv(sk_client, buf, sizeof(buf), &incoming1); + c_assert(!r); + c_assert(incoming1); + + test_poll(sk_client); + + r = n_dhcp4_c_socket_packet_recv(sk_client, buf, sizeof(buf), &incoming2); + c_assert(!r); + c_assert(incoming2); + + /* teardown */ + + link_del_ip4(link_server, &addr_server, 8); +} + +static void test_server_client_udp(Link *link_server, Link *link_client) { + _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL; + _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL; + _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1; + struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) }; + struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) }; + uint8_t buf[UINT16_MAX]; + int r; + + /* setup */ + + link_add_ip4(link_server, &addr_server, 8); + link_add_ip4(link_client, &addr_client, 8); + + /* test communication */ + + test_server_udp_socket_new(link_server, &sk_server); + test_client_udp_socket_new(link_client, &sk_client, &addr_client, &addr_server); + + r = n_dhcp4_outgoing_new(&outgoing, 0, 0); + c_assert(!r); + n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREPLY; + + r = n_dhcp4_s_socket_udp_send(sk_server, + &addr_server, + &addr_client, + outgoing); + c_assert(!r); + + test_poll(sk_client); + + r = n_dhcp4_c_socket_udp_recv(sk_client, buf, sizeof(buf), &incoming); + c_assert(!r); + c_assert(incoming); + + /* teardown */ + + link_del_ip4(link_client, &addr_client, 8); + link_del_ip4(link_server, &addr_server, 8); +} + +static void test_sockets(void) { + _c_cleanup_(netns_closep) int ns_server = -1, ns_client = -1; + _c_cleanup_(link_deinit) Link link_server = LINK_NULL(link_server); + _c_cleanup_(link_deinit) Link link_client = LINK_NULL(link_client); + + /* setup */ + + netns_new(&ns_server); + netns_new(&ns_client); + link_new_veth(&link_server, &link_client, ns_server, ns_client); + + /* communication tests */ + + test_client_server_packet(&link_server, &link_client); + test_client_server_udp(&link_server, &link_client); + test_server_client_packet(&link_server, &link_client); + test_server_client_udp(&link_server, &link_client); +} + +static void test_multiple_servers(void) { + _c_cleanup_(netns_closep) int netns = -1; + _c_cleanup_(link_deinit) Link link_server = LINK_NULL(link_server); + _c_cleanup_(link_deinit) Link link_client = LINK_NULL(link_client); + int r, oldns; + + /* setup */ + + netns_new(&netns); + link_new_veth(&link_server, &link_client, netns, netns); + + /* test multiple server UDP sockets on the same machine */ + + netns_get(&oldns); + netns_set(netns); + { + _c_cleanup_(c_closep) int sk1 = -1, sk2 = -1; + + /* + * DHCP servers have to bind to a fixed port, so you cannot run + * two servers on the same interface. It must be possible to + * run them on separate interfaces, though. + */ + + r = n_dhcp4_s_socket_udp_new(&sk1, link_server.ifindex); + c_assert(r >= 0); + + r = n_dhcp4_s_socket_udp_new(&sk2, link_server.ifindex); + c_assert(r == -EADDRINUSE); + + r = n_dhcp4_s_socket_udp_new(&sk2, link_client.ifindex); + c_assert(r >= 0); + } + netns_set(oldns); +} + +int main(int argc, char **argv) { + test_setup(); + + test_sockets(); + test_multiple_servers(); + + return 0; +} diff --git a/shared/n-dhcp4/src/test.h b/shared/n-dhcp4/src/test.h new file mode 100644 index 000000000..6933982e8 --- /dev/null +++ b/shared/n-dhcp4/src/test.h @@ -0,0 +1,107 @@ +#pragma once + +/* + * Test Helpers + * Bunch of helpers to setup the environment for networking tests. This + * includes net-namespace setups, veth setups, and more. + */ + +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static inline void test_raise_memlock(void) { + const size_t wanted = 64 * 1024 * 1024; + struct rlimit get, set; + int r; + + r = getrlimit(RLIMIT_MEMLOCK, &get); + c_assert(!r); + + /* try raising limit to @wanted */ + set.rlim_cur = wanted; + set.rlim_max = (wanted > get.rlim_max) ? wanted : get.rlim_max; + r = setrlimit(RLIMIT_MEMLOCK, &set); + if (r) { + c_assert(errno == EPERM); + + /* not privileged to raise limit, so maximize soft limit */ + set.rlim_cur = get.rlim_max; + set.rlim_max = get.rlim_max; + r = setrlimit(RLIMIT_MEMLOCK, &set); + c_assert(!r); + } +} + +static inline void test_unshare_user_namespace(void) { + uid_t euid; + gid_t egid; + int r, fd; + + /* + * Enter a new user namespace as root:root. + */ + + euid = geteuid(); + egid = getegid(); + + r = unshare(CLONE_NEWUSER); + c_assert(r >= 0); + + fd = open("/proc/self/uid_map", O_WRONLY); + c_assert(fd >= 0); + r = dprintf(fd, "0 %d 1\n", euid); + c_assert(r >= 0); + close(fd); + + fd = open("/proc/self/setgroups", O_WRONLY); + c_assert(fd >= 0); + r = dprintf(fd, "deny"); + c_assert(r >= 0); + close(fd); + + fd = open("/proc/self/gid_map", O_WRONLY); + c_assert(fd >= 0); + r = dprintf(fd, "0 %d 1\n", egid); + c_assert(r >= 0); + close(fd); +} + +static inline void test_setup(void) { + int r; + + /* + * Move into a new network and mount namespace both associated + * with a new user namespace where the current eUID is mapped to + * 0. Then create a a private instance of /run/netns. This ensures + * that any network devices or network namespaces are private to + * the test process. + */ + + test_raise_memlock(); + test_unshare_user_namespace(); + + r = unshare(CLONE_NEWNET | CLONE_NEWNS); + c_assert(r >= 0); + + r = mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL); + c_assert(r >= 0); + + r = mount(NULL, "/run", "tmpfs", 0, NULL); + c_assert(r >= 0); + + r = mkdir("/run/netns", 0755); + c_assert(r >= 0); +} diff --git a/shared/n-dhcp4/src/util/link.c b/shared/n-dhcp4/src/util/link.c new file mode 100644 index 000000000..2a3ac7fe1 --- /dev/null +++ b/shared/n-dhcp4/src/util/link.c @@ -0,0 +1,287 @@ +/* + * Link Management + * + * This is for our test-infrastructure only! It is not meant to be used outside + * of unit-tests. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "link.h" +#include "netns.h" +#include "socket.h" + +/** + * link_deinit() - deinitialize link + * @link: link to operate on + * + * This deinitializes a link and clears it. Once this call returns the link is + * cleared to LINK_NULL(). + * + * It is safe to call this on LINK_NULL(), in which case it is a no-op. It is + * thus also safe to call this multiple times on the same link. + */ +void link_deinit(Link *link) { + netns_close(link->netns); + *link = (Link)LINK_NULL(*link); +} + +static void link_query(int netns, const char *name, int *ifindexp, struct ether_addr *macp) { + int oldns; + + netns_get(&oldns); + { + struct ifreq ifr = {}; + size_t n_name; + int r, s; + + netns_set(netns); + + n_name = strlen(name); + c_assert(n_name <= IF_NAMESIZE); + + if (ifindexp) { + *ifindexp = if_nametoindex(name); + c_assert(*ifindexp > 0); + } + + if (macp) { + s = socket(AF_INET, SOCK_DGRAM, 0); + c_assert(s >= 0); + + strncpy(ifr.ifr_name, name, n_name); + r = ioctl(s, SIOCGIFHWADDR, &ifr); + c_assert(r >= 0); + + memcpy(macp->ether_addr_octet, ifr.ifr_hwaddr.sa_data, ETH_ALEN); + + close(s); + } + } + netns_set(oldns); +} + +static void link_move(const char *ifname, int netns) { + char *p; + int r; + + r = asprintf(&p, "ip link set %s up netns ns-test", ifname); + c_assert(r > 0); + + netns_pin(netns, "ns-test"); + r = system(p); + c_assert(r == 0); + netns_unpin("ns-test"); + + free(p); +} + +/** + * link_new_veth() - create new veth pair + * @veth_parentp: output argument for new veth parent + * @veth_childp: output argument for new veth child + * @netns_parent: target namespace for the parent + * @netns_child: target namespace for the child + * + * This creates a new veth pair in the specified namespaces. + */ +void link_new_veth(Link *veth_parentp, Link *veth_childp, int netns_parent, int netns_child) { + int oldns; + + netns_get(&oldns); + { + int r; + + /* + * Temporarily enter a new network namespace to make sure the + * interface names are fresh. + */ + netns_set_anonymous(); + + r = system("ip link add veth-parent type veth peer name veth-child"); + c_assert(r == 0); + r = system("ip link set veth-parent up addrgenmode none"); + c_assert(r == 0); + r = system("ip link set veth-child up addrgenmode none"); + c_assert(r == 0); + + link_move("veth-parent", netns_parent); + link_move("veth-child", netns_child); + } + netns_set(oldns); + + netns_new_dup(&veth_parentp->netns, netns_parent); + netns_new_dup(&veth_childp->netns, netns_child); + link_query(netns_parent, "veth-parent", &veth_parentp->ifindex, &veth_parentp->mac); + link_query(netns_child, "veth-child", &veth_childp->ifindex, &veth_childp->mac); + + /* + * XXX: After moving a link both its name and ifindex might have + * changed. Hence, link_query() might check the wrong interface. + * One way to fix this would be to rename the interfaces after + * they have been moved and queried based on their final ifindex. + * This way, we reserve the internal names for the constructor, + * and guarantee the final names will never conflict (disallowing + * parallel calls to this function). + */ +} + +/** + * link_new_bridge() - create new bridge + * @bridgep: output argument for the new bridge + * @netns: target network namespace + * + * This creates a new bridge interface in the specified target network + * namespace. + */ +void link_new_bridge(Link *bridgep, int netns) { + int oldns; + + netns_get(&oldns); + { + int r; + + netns_set(netns); + + r = system("ip link add test-bridge type bridge"); + c_assert(r == 0); + r = system("ip link set test-bridge up addrgenmode none"); + c_assert(r == 0); + } + netns_set(oldns); + + netns_new_dup(&bridgep->netns, netns); + link_query(netns, "test-bridge", &bridgep->ifindex, &bridgep->mac); +} + +/** + * link_add_ip4() - add IPv4 address to the specified link + * @link: link to operate on + * @addr: address to add + * @prefix: address prefix length + * + * This adds the specified IPv4 address to the given link. + */ +void link_add_ip4(Link *link, const struct in_addr *addr, unsigned int prefix) { + int oldns; + + netns_get(&oldns); + { + char *p, ifname[IF_NAMESIZE + 1] = {}; + int r; + + netns_set(link->netns); + + p = if_indextoname(link->ifindex, ifname); + c_assert(p); + r = asprintf(&p, "ip addr add %s/%u dev %s", inet_ntoa(*addr), prefix, ifname); + c_assert(r >= 0); + r = system(p); + c_assert(r == 0); + free(p); + } + netns_set(oldns); +} + +/** + * link_del_ip4() - delete IPv4 address from the specified link + * @link: link to operate on + * @addr: address to delete + * @prefix: address prefix length + * + * This deletes the specified IPv4 address from the given link. + */ +void link_del_ip4(Link *link, const struct in_addr *addr, unsigned int prefix) { + int oldns; + + netns_get(&oldns); + { + char *p, ifname[IF_NAMESIZE + 1] = {}; + int r; + + netns_set(link->netns); + + p = if_indextoname(link->ifindex, ifname); + c_assert(p); + r = asprintf(&p, "ip addr del %s/%u dev %s", inet_ntoa(*addr), prefix, ifname); + c_assert(r >= 0); + r = system(p); + c_assert(r == 0); + free(p); + } + netns_set(oldns); +} + +/** + * link_set_master() - change the bridge master of an interface + * @link: link to operate on + * @if_master: bridge to set as master + * + * This sets @if_master as the new master bridge of @link. The specified bridge + * must be in the same network namespace as @link. + */ +void link_set_master(Link *link, int if_master) { + int oldns; + + netns_get(&oldns); + { + char *p, ifname_master[IF_NAMESIZE + 1] = {}, ifname[IF_NAMESIZE + 1] = {}; + int r; + + netns_set(link->netns); + + p = if_indextoname(link->ifindex, ifname); + c_assert(p); + p = if_indextoname(if_master, ifname_master); + c_assert(p); + r = asprintf(&p, "ip link set %s master %s", ifname, ifname_master); + c_assert(r > 0); + r = system(p); + c_assert(r == 0); + free(p); + } + netns_set(oldns); +} + +/** + * link_socket() - create socket for link + * @link: link to operate on + * @socketp: output argument for new socket + * @family: socket family to create socket in + * @type: socket type to create socket as + * + * This creates a socket of the protocol family @family via socket(2), but + * makes sure to create it in the network-namespace where @link resides. + * Furthermore, the socket is bound to the link specified in @link. + * + * The new socket is returned in @socketp. + */ +void link_socket(Link *link, int *socketp, int family, int type) { + int oldns; + + netns_get(&oldns); + { + int r, fd; + + netns_set(link->netns); + + fd = socket(family, type, 0); + c_assert(fd >= 0); + + r = socket_bind_if(fd, link->ifindex); + c_assert(!r); + + *socketp = fd; + } + netns_set(oldns); +} diff --git a/shared/n-dhcp4/src/util/link.h b/shared/n-dhcp4/src/util/link.h new file mode 100644 index 000000000..cd0ad6f84 --- /dev/null +++ b/shared/n-dhcp4/src/util/link.h @@ -0,0 +1,38 @@ +#pragma once + +/* + * Link Management + * + * This utility provides easy access to network links. It is meant for testing + * purposes only and relies on call-outs to ip(1). A proper implementation + * should rather use netlink directly to interact with the kernel. + * + * Furthermore, for simplification this is limited to ethernet links. + */ + +#include +#include +#include +#include + +typedef struct Link Link; + +struct Link { + int netns; + int ifindex; + struct ether_addr mac; +}; + +#define LINK_NULL(_x) { \ + .netns = -1, \ + } + +void link_deinit(Link *link); + +void link_new_veth(Link *veth_parentp, Link *veth_childp, int netns_parent, int netns_child); +void link_new_bridge(Link *bridgep, int netns); + +void link_add_ip4(Link *link, const struct in_addr *addr, unsigned int prefix); +void link_del_ip4(Link *link, const struct in_addr *addr, unsigned int prefix); +void link_set_master(Link *link, int if_master); +void link_socket(Link *link, int *socketp, int family, int type); diff --git a/shared/n-dhcp4/src/util/netns.c b/shared/n-dhcp4/src/util/netns.c new file mode 100644 index 000000000..0b7b32e43 --- /dev/null +++ b/shared/n-dhcp4/src/util/netns.c @@ -0,0 +1,165 @@ +/* + * Network Namespaces + * + * This is meant for testing-purposes only. It is not meant to be used outside + * of our unit-tests! + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "netns.h" + +/** + * netns_new() - create a new network namespace + * @netnsp: output argument to store netns fd + * + * This creates a new network namespace and returns a netns fd that refers to + * the new network namespace. Note that there is no native API to create an + * anonymous network namespace, so this call has to temporarily switch to a new + * network namespace (using unshare(2)). This temporary switch does not affect + * any other threads or processes, however, it can be observed by other + * processes. + */ +void netns_new(int *netnsp) { + int r, oldns; + + netns_get(&oldns); + + r = unshare(CLONE_NEWNET); + c_assert(r >= 0); + + netns_get(netnsp); + netns_set(oldns); +} + +/** + * netns_new_dup() - duplicate network namespace descriptor + * @newnsp: output argument for duplicated descriptor + * @netns: netns descriptor to duplicate + * + * This duplicates the network namespace file descriptor. The duplicate still + * refers to the same network namespace, but is an independent file descriptor. + */ +void netns_new_dup(int *newnsp, int netns) { + *newnsp = fcntl(netns, F_DUPFD_CLOEXEC, 0); + c_assert(*newnsp >= 0); +} + +/** + * netns_close() - destroy a network namespace descriptor + * @netns: netns to operate on, or <0 + * + * This closes the given network namespace descriptor. If @netns is negative, + * this is a no-op. + * + * Return: -1 is returned. + */ +int netns_close(int netns) { + return c_close(netns); +} + +/** + * netns_get() - retrieve the current network namespace + * @netnsp: output argument to store netns fd + * + * This retrieves a file-descriptor to the current network namespace. + */ +void netns_get(int *netnsp) { + *netnsp = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC); + c_assert(*netnsp >= 0); +} + +/** + * setns_set() - change the current network namespace + * @netns: netns to set + * + * This changes the current network namespace to the netns given by the + * file-descriptor @netns. + */ +void netns_set(int netns) { + int r; + + r = setns(netns, CLONE_NEWNET); + c_assert(r >= 0); +} + +/** + * netns_set_anonymous() - enter an anonymous network namespace + * + * This is a helper that creates a new network namespace, enters it, and then + * forgets about it. + */ +void netns_set_anonymous(void) { + int r; + + r = unshare(CLONE_NEWNET); + c_assert(r >= 0); +} + +/** + * netns_pin() - pin network namespace in file-system + * @netns: netns to pin + * @name: name to pin netns under + * + * This pins the network namespace given as @netns in the file-system as + * `/run/netns/@name`. It is the responsibility of the caller to guarantee + * @name is not used by anyone else in parallel. This function will abort if + * @name is already in use. + * + * The namespace in `/run/netns/` is compatible with the namespace provided by + * the ip(1) tool, and can be used to pass network namespaces to invocations of + * ip(1). + */ +void netns_pin(int netns, const char *name) { + char *fd_path, *netns_path; + int r, fd; + + r = asprintf(&fd_path, "/proc/self/fd/%d", netns); + c_assert(r >= 0); + + r = asprintf(&netns_path, "/run/netns/%s", name); + c_assert(r >= 0); + + fd = open(netns_path, O_RDONLY|O_CLOEXEC|O_CREAT|O_EXCL, 0); + c_assert(fd >= 0); + close(fd); + + r = mount(fd_path, netns_path, "none", MS_BIND, NULL); + c_assert(r >= 0); + + free(netns_path); + free(fd_path); +} + +/** + * netns_unpin() - unpin network namespace from file-system + * @name: name to unpin + * + * This removes a network namespace pin from the file-system. It expects the + * pin to be located at `/run/netns/@name`. This function aborts if the pin + * does not exist. + * + * See netns_pin() for ways to create such pins. + */ +void netns_unpin(const char *name) { + char *netns_path; + int r; + + r = asprintf(&netns_path, "/run/netns/%s", name); + c_assert(r >= 0); + + r = umount2(netns_path, MNT_DETACH); + c_assert(r >= 0); + + r = unlink(netns_path); + c_assert(r >= 0); + + free(netns_path); +} diff --git a/shared/n-dhcp4/src/util/netns.h b/shared/n-dhcp4/src/util/netns.h new file mode 100644 index 000000000..02e2e0108 --- /dev/null +++ b/shared/n-dhcp4/src/util/netns.h @@ -0,0 +1,27 @@ +#pragma once + +/* + * Network Namespaces + * + * The netns utility provides an object-based API to network namespaces. It is + * meant for testing purposes only. + */ + +#include +#include + +void netns_new(int *netnsp); +void netns_new_dup(int *newnsp, int netns); +int netns_close(int netns); + +void netns_get(int *netnsp); +void netns_set(int netns); +void netns_set_anonymous(void); + +void netns_pin(int netns, const char *name); +void netns_unpin(const char *name); + +static inline void netns_closep(int *netns) { + if (*netns >= 0) + netns_close(*netns); +} diff --git a/shared/n-dhcp4/src/util/packet.c b/shared/n-dhcp4/src/util/packet.c new file mode 100644 index 000000000..38cb399d5 --- /dev/null +++ b/shared/n-dhcp4/src/util/packet.c @@ -0,0 +1,456 @@ +/* + * Packet Sockets + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "packet.h" + +/** + * packet_internet_checksum() - compute the internet checksum + * @data: the data to checksum + * @size: the length of @data in bytes + * + * Computes the internet checksum for a given blob according to RFC1071. + * + * The internet checksum is the one's complement of the one's complement sum of + * the 16-bit words of the data, padded with zero-bytes if the data does not + * end on a 16-bit boundary. + * + * Return: Checksum is returned. + */ +uint16_t packet_internet_checksum(const uint8_t *data, size_t size) { + uint64_t acc = 0; + uint32_t local; + + while (size >= sizeof(local)) { + memcpy(&local, data, sizeof(local)); + acc += local; + + data += sizeof(local); + size -= sizeof(local); + } + + if (size) { + local = 0; + memcpy(&local, data, size); + acc += local; + } + + while (acc >> 16) + acc = (acc & 0xffff) + (acc >> 16); + + return ~acc; +} + +/** + * packet_internet_checksum_udp() - compute the internet checkum for UDP packets + * @src_addr: source IP address + * @dst_addr: destination IP address + * @src_port: source port + * @dst_port: destination port + * @data: payload + * @size: length of payload in bytes + * @checksum: current checksum, or 0 + * + * Computes the internet checksum for a UDP packet, given the relevant IP and + * UDP header fields. + * + * Note that since a UDP packet contains the checksum itself, the resulting + * checksum will always be 0 (this fact is used to verify that a UDP packet is + * valid). + * Inversely, when calculating the checksum for outgoing packets, you have to + * specify 0 as @checksum, and this function will return the checksum for the + * caller to use for the packet. In this case, though, the caller must check + * whether the returned checksum might coincidentally be 0, in which case it + * must be flipped to -1 (0xffff), since 0 is not allowed as checksum in UDP + * packets, and -1 is arithmetically equivalent in the checksum calculation. + * + * Return: Checksum is returned. + */ +uint16_t packet_internet_checksum_udp(const struct in_addr *src_addr, + const struct in_addr *dst_addr, + uint16_t src_port, + uint16_t dst_port, + const uint8_t *data, + size_t size, + uint16_t checksum) { + struct { + uint32_t src; + uint32_t dst; + uint8_t _zeros; + uint8_t protocol; + uint16_t length; + struct udphdr udp; + } _c_packed_ udp_phdr = { + .src = src_addr->s_addr, + .dst = dst_addr->s_addr, + .protocol = IPPROTO_UDP, + .length = htons(sizeof(struct udphdr) + size), + .udp = { + .source = htons(src_port), + .dest = htons(dst_port), + .len = htons(sizeof(struct udphdr) + size), + .check = checksum, + }, + }; + const uint8_t *iter; + uint64_t acc = 0; + uint32_t local; + + _Static_assert(!(sizeof(udp_phdr) % sizeof(local)), + "UDP header structure size is not a multiple of 4"); + + for (iter = (const uint8_t *)&udp_phdr; + iter < (const uint8_t *)(&udp_phdr + 1); + iter += sizeof(local)) { + memcpy(&local, iter, sizeof(local)); + acc += local; + } + + while (size >= sizeof(local)) { + memcpy(&local, data, sizeof(local)); + acc += local; + + data += sizeof(local); + size -= sizeof(local); + } + + if (size) { + local = 0; + memcpy(&local, data, size); + acc += local; + } + + while (acc >> 16) + acc = (acc & 0xffff) + (acc >> 16); + + return ~acc; +} + +/** + * packet_sendto_udp() - send UDP packet on AF_PACKET socket + * @sockfd: AF_PACKET/SOCK_DGRAM socket + * @buf: payload + * @n_buf: length of payload in bytes + * @n_transmittedp: output argument for number of transmitted bytes + * @src_paddr: source protocol address, see ip(7) + * @dest_haddr: destination hardware address, see packet(7) + * @dest_paddr: destination protocol address, see ip(7) + * + * Sends an UDP packet on a AF_PACKET socket directly to a hardware + * address. The difference between this and sendto() on an AF_INET + * socket is that no routing is performed, so the packet is delivered + * even if the destination IP is not yet configured on the destination + * host. + * + * Return: 0 on success, negative error code on failure. + */ +int packet_sendto_udp(int sockfd, + const void *buf, + size_t n_buf, + size_t *n_transmittedp, + const struct sockaddr_in *src_paddr, + const struct packet_sockaddr_ll *dest_haddr, + const struct sockaddr_in *dest_paddr) { + struct iphdr ip_hdr = { + .version = IPVERSION, + .ihl = sizeof(ip_hdr) / 4, /* Length of header in multiples of four bytes */ + .tos = IPTOS_CLASS_CS6, /* Class Selector for network control */ + .tot_len = htons(sizeof(struct iphdr) + sizeof(struct udphdr) + n_buf), + .frag_off = htons(IP_DF), /* Do not fragment */ + .ttl = IPDEFTTL, + .protocol = IPPROTO_UDP, + .saddr = src_paddr->sin_addr.s_addr, + .daddr = dest_paddr->sin_addr.s_addr, + }; + struct udphdr udp_hdr = { + .source = src_paddr->sin_port, + .dest = dest_paddr->sin_port, + .len = htons(sizeof(udp_hdr) + n_buf), + }; + struct iovec iov[3] = { + { + .iov_base = &ip_hdr, + .iov_len = sizeof(ip_hdr), + }, + { + .iov_base = &udp_hdr, + .iov_len = sizeof(udp_hdr), + }, + { + .iov_base = (void *)buf, + .iov_len = n_buf, + }, + }; + struct msghdr msg = { + .msg_name = (void*)dest_haddr, + .msg_namelen = sizeof(*dest_haddr), + .msg_iov = iov, + .msg_iovlen = sizeof(iov) / sizeof(iov[0]), + }; + ssize_t pktlen; + + ip_hdr.check = packet_internet_checksum((void*)&ip_hdr, sizeof(ip_hdr)); + udp_hdr.check = packet_internet_checksum_udp(&src_paddr->sin_addr, + &dest_paddr->sin_addr, + ntohs(src_paddr->sin_port), + ntohs(dest_paddr->sin_port), + buf, + n_buf, + 0); + + /* + * 0x0000 and 0xffff are equivalent for computing the UDP checksum, + * but 0x0000 is reserved in UDP headers, to mean that the checksum is + * not set and should be ignored by the receiver. Hence, flip it to + * 0xffff in that case. + */ + udp_hdr.check = udp_hdr.check ?: 0xffff; + + pktlen = sendmsg(sockfd, &msg, 0); + if (pktlen < 0) + return -errno; + + /* + * Kernel never truncates. Worst case, we get -EMSGSIZE. Kernel *might* + * prepend VNET headers, in which case a bigger length than sent is + * returned. + * Lets assert on this, and then return to the caller the proportion of + * its own buffer that we sent (which is always exactly the requested + * size). + */ + c_assert((size_t)pktlen >= sizeof(ip_hdr) + sizeof(udp_hdr) + n_buf); + *n_transmittedp = n_buf; + return 0; +} + +/** + * packet_recvfrom_upd() - receive UDP packet from AF_PACKET socket + * @sockfd: AF_PACKET/SOCK_DGRAM socket + * @buf: buffor for payload + * @n_buf: max length of payload in bytes + * @n_transmittedp: output argument for number transmitted bytes + * @src: return argumnet for source address, or NULL, see ip(7) + * + * Receives an UDP packet on a AF_PACKET socket. The difference between + * this and recvfrom() on an AF_INET socket is that the packet will be + * received even if the destination IP address has not been configured + * on the interface. + * + * Return: 0 on success, negative error code on failure. + */ +int packet_recvfrom_udp(int sockfd, + void *buf, + size_t n_buf, + size_t *n_transmittedp, + struct sockaddr_in *src) { + union { + struct iphdr hdr; + /* + * Maximum IP-header length is 15 * 4, since it is specified in + * the `ihl` field, which is four bits and interpreted as + * factor of 4. So maximum `ihl` value is `(2^4 - 1) * 4`. + */ + uint8_t data[15 * 4]; + } ip_hdr; + struct udphdr udp_hdr; + struct iovec iov[3] = { + { + .iov_base = &ip_hdr, + }, + { + .iov_base = &udp_hdr, + .iov_len = sizeof(udp_hdr), + }, + { + .iov_base = buf, + .iov_len = n_buf, + }, + }; + uint8_t cmsgbuf[CMSG_LEN(sizeof(struct tpacket_auxdata))]; + struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = sizeof(iov) / sizeof(iov[0]), + .msg_control = cmsgbuf, + .msg_controllen = sizeof(cmsgbuf), + }; + struct cmsghdr *cmsg; + bool checksum = true; + ssize_t pktlen; + size_t hdrlen; + + /* Peek packet to obtain the real IP header length */ + pktlen = recv(sockfd, &ip_hdr.hdr, sizeof(ip_hdr.hdr), MSG_PEEK); + if (pktlen < 0) + return -errno; + + if ((size_t)pktlen < sizeof(ip_hdr.hdr)) { + /* + * Received packet is smaller than the minimal IP header length, + * discard it. + */ + recv(sockfd, NULL, 0, 0); + *n_transmittedp = 0; + return 0; + } + + if (ip_hdr.hdr.version != IPVERSION) { + /* + * This is not an IPv4 packet, discard it. + */ + recv(sockfd, NULL, 0, 0); + *n_transmittedp = 0; + return 0; + } + + hdrlen = ip_hdr.hdr.ihl * 4; + if (hdrlen < sizeof(ip_hdr.hdr)) { + /* + * The length given in the header is smaller than the minimum + * header length, discard the packet. + */ + recv(sockfd, NULL, 0, 0); + *n_transmittedp = 0; + return 0; + } + + /* + * Now that we know the ip-header length, we can prepare the iovec to + * read the entire packet into the correct buffers. + */ + iov[0].iov_len = hdrlen; + pktlen = recvmsg(sockfd, &msg, 0); + if (pktlen < 0) + return -errno; + + cmsg = CMSG_FIRSTHDR(&msg); + if (cmsg) { + if (cmsg->cmsg_level == SOL_PACKET && + cmsg->cmsg_type == PACKET_AUXDATA && + cmsg->cmsg_len == CMSG_LEN(sizeof(struct tpacket_auxdata))) { + struct tpacket_auxdata *aux = (void *)CMSG_DATA(cmsg); + checksum = !(aux->tp_status & TP_STATUS_CSUMNOTREADY); + } + } + + if (ntohs(ip_hdr.hdr.tot_len) > pktlen) { + /* + * The IP-packet is bigger than the chunk returned by the + * kernel. So either the packet is corrupt, or our caller + * provided too small a buffer. In both cases, we simply drop + * the packet. + */ + *n_transmittedp = 0; + return 0; + } + + /* Truncate trailing garbage. */ + pktlen = ntohs(ip_hdr.hdr.tot_len); + + if ((size_t)pktlen < hdrlen + sizeof(udp_hdr)) { + /* + * The packet is too small to even contain an entire UDP + * header, so discard it entirely. + */ + *n_transmittedp = 0; + return 0; + } else if ((size_t)pktlen < hdrlen + ntohs(udp_hdr.len)) { + /* + * The UDP header specified a longer length than the returned + * packet, so discard it entirely. + */ + *n_transmittedp = 0; + return 0; + } + + /* + * Make @pktlen the length of the packet payload, without IP/UDP + * headers, since that is what the caller is interested in. + */ + pktlen = ntohs(udp_hdr.len) - sizeof(struct udphdr); + + /* IP */ + + if (ip_hdr.hdr.protocol != IPPROTO_UDP) { + *n_transmittedp = 0; + return 0; /* not a UDP packet, discard it */ + } else if (ip_hdr.hdr.frag_off & htons(IP_MF | IP_OFFMASK)) { + *n_transmittedp = 0; + return 0; /* fragmented packet, discard it */ + } else if (checksum && packet_internet_checksum(ip_hdr.data, hdrlen)) { + *n_transmittedp = 0; + return 0; /* invalid checksum, discard it */ + } + + /* UDP */ + + if (checksum && udp_hdr.check) { + /* + * Computing the checksum of a packet that has the checksum set + * must yield 0. If it does not yield 0, the packet is invalid, + * in which case we discard it. + */ + if (packet_internet_checksum_udp(&(struct in_addr){ ip_hdr.hdr.saddr }, + &(struct in_addr){ ip_hdr.hdr.daddr }, + ntohs(udp_hdr.source), + ntohs(udp_hdr.dest), + buf, + pktlen, + udp_hdr.check)) { + *n_transmittedp = 0; + return 0; + } + } + + if (src) { + src->sin_family = AF_INET; + src->sin_addr.s_addr = ip_hdr.hdr.saddr; + src->sin_port = udp_hdr.source; + } + + /* Return length of UDP payload (i.e., data written to @buf). */ + *n_transmittedp = pktlen; + return 0; +} + +/** + * packet_shutdown() - shutdown socket for future receive operations + * @sockfd: socket + * + * Partially emulates `shutdown(sockfd, SHUT_RD)`, in the sense that no + * further packets may be queued on the socket. All packets that are + * already queued will still be delivered, but once -EAGAIN is returned + * we are guaranteed never to be able to read more packets in the future. + * + * Return: 0 on success, or a negative error code on failure. + */ +int packet_shutdown(int sockfd) { + struct sock_filter filter[] = { + BPF_STMT(BPF_RET + BPF_K, 0), /* discard all packets */ + }; + struct sock_fprog fprog = { + .filter = filter, + .len = sizeof(filter) / sizeof(filter[0]), + }; + int r; + + r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)); + if (r < 0) + return -errno; + + return 0; +} diff --git a/shared/n-dhcp4/src/util/packet.h b/shared/n-dhcp4/src/util/packet.h new file mode 100644 index 000000000..edbf2a3e4 --- /dev/null +++ b/shared/n-dhcp4/src/util/packet.h @@ -0,0 +1,62 @@ +#pragma once + +/* + * Packet Sockets + */ + +#include +#include +#include +#include +#include +#include +#include + +/* + * `struct sockaddr_ll` is too small to fit the Infiniband hardware address. + * Introduce `struct packet_sockaddr_ll` which is the same as the original, + * except the `sl_addr` field is extended to fit all the supported hardware + * addresses. + */ +struct packet_sockaddr_ll { + unsigned short sll_family; + __be16 sll_protocol; + int sll_ifindex; + unsigned short sll_hatype; + unsigned char sll_pkttype; + unsigned char sll_halen; + unsigned char sll_addr[MAX_ADDR_LEN]; +}; + +uint16_t packet_internet_checksum(const uint8_t *data, size_t len); +uint16_t packet_internet_checksum_udp(const struct in_addr *src_addr, + const struct in_addr *dst_addr, + uint16_t src_port, + uint16_t dst_port, + const uint8_t *data, + size_t size, + uint16_t checksum); + +int packet_sendto_udp(int sockfd, + const void *buf, + size_t n_buf, + size_t *n_transmittedp, + const struct sockaddr_in *src_paddr, + const struct packet_sockaddr_ll *dest_haddr, + const struct sockaddr_in *dest_paddr); +int packet_recvfrom_udp(int sockfd, + void *buf, + size_t n_buf, + size_t *n_transmittedp, + struct sockaddr_in *src); + +int packet_shutdown(int sockfd); + +/* inline helpers */ + +static inline int packet_recv_udp(int sockfd, + void *buf, + size_t n_buf, + size_t *n_transmittedp) { + return packet_recvfrom_udp(sockfd, buf, n_buf, n_transmittedp, NULL); +} diff --git a/shared/n-dhcp4/src/util/socket.c b/shared/n-dhcp4/src/util/socket.c new file mode 100644 index 000000000..c25f76fdd --- /dev/null +++ b/shared/n-dhcp4/src/util/socket.c @@ -0,0 +1,121 @@ +/* + * Socket Utilities + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "socket.h" + +/** + * socket_SIOCGIFNAME() - resolve an ifindex to an ifname + * @socket: socket to operate on + * @ifindex: index of network interface to resolve + * @ifname: buffer to store resolved name + * + * This uses the SIOCGIFNAME ioctl to resolve an ifindex to an ifname. The + * buffer provided in @ifnamep must be at least IFNAMSIZ bytes in size. The + * maximum ifname length is IFNAMSIZ-1, and this function always + * zero-terminates the result. + * + * This function is similar to if_indextoname(3) provided by glibc, but it + * allows to specify the target socket explicitly. This allows the caller to + * control the target network-namespace, rather than relying on the network + * namespace of the running process. + * + * Return: 0 on success, negative kernel error code on failure. + */ +int socket_SIOCGIFNAME(int socket, int ifindex, char (*ifnamep)[IFNAMSIZ]) { + struct ifreq req = { .ifr_ifindex = ifindex }; + int r; + + r = ioctl(socket, SIOCGIFNAME, &req); + if (r < 0) + return -errno; + + /* + * The linux kernel guarantees that an interface name is always + * zero-terminated, and it always fully fits into IFNAMSIZ bytes, + * including the zero-terminator. + */ + memcpy(ifnamep, req.ifr_name, IFNAMSIZ); + return 0; +} + +/** + * socket_bind_if() - bind socket to a network interface + * @socket: socket to operate on + * @ifindex: index of network interface to bind to, or 0 + * + * This binds the socket given via @socket to the network interface specified + * via @ifindex. It uses the underlying SO_BINDTODEVICE ioctl of the linux + * kernel. However, if available, if prefers the newer SO_BINDTOIFINDEX ioctl, + * which avoids resolving the interface name temporarily, and thus does not + * suffer from a race-condition. + * + * Return: 0 on success, negative error code on failure. + */ +int socket_bind_if(int socket, int ifindex) { + char ifname[IFNAMSIZ] = {}; + int r; + + c_assert(ifindex >= 0); + + /* + * We first try the newer SO_BINDTOIFINDEX. If it is not available on + * the running kernel, we fall back to SO_BINDTODEVICE. This, however, + * requires us to first resolve the ifindex to an ifname. Note that + * this is racy, since the device name might theoretically change + * asynchronously. + * + * Using 0 as ifindex will remove the device-binding. For + * SO_BINDTOIFINDEX we simply pass-through the 0 to the kernel, which + * recognizes this correctly. For SO_BINDTODEVICE we pass the empty + * string, which the kernel recognizes as a request to remove the + * binding. + * + * The commit introducing SO_BINDTOIFINDEX first appeared in linux-5.1: + * + * commit f5dd3d0c9638a9d9a02b5964c4ad636f06cf7e2c + * Author: David Herrmann + * Date: Tue Jan 15 14:42:14 2019 +0100 + * + * net: introduce SO_BINDTOIFINDEX sockopt + * + * In older kernels, setsockopt(2) is guaranteed to return ENOPROTOOPT + * for this ioctl. + */ + +#ifdef SO_BINDTOIFINDEX + r = setsockopt(socket, + SOL_SOCKET, + SO_BINDTOIFINDEX, + &ifindex, + sizeof(ifindex)); + if (r >= 0) + return 0; + else if (errno != ENOPROTOOPT) + return -errno; +#endif /* SO_BINDTOIFINDEX */ + + if (ifindex > 0) { + r = socket_SIOCGIFNAME(socket, ifindex, &ifname); + if (r) + return r; + } + + r = setsockopt(socket, + SOL_SOCKET, + SO_BINDTODEVICE, + ifname, + strlen(ifname)); + if (r < 0) + return -errno; + + return 0; +} diff --git a/shared/n-dhcp4/src/util/socket.h b/shared/n-dhcp4/src/util/socket.h new file mode 100644 index 000000000..b5ecf2c21 --- /dev/null +++ b/shared/n-dhcp4/src/util/socket.h @@ -0,0 +1,11 @@ +#pragma once + +/* + * Socket Utilities + */ + +#include +#include + +int socket_SIOCGIFNAME(int socket, int ifindex, char (*ifnamep)[IFNAMSIZ]); +int socket_bind_if(int socket, int ifindex); diff --git a/shared/n-dhcp4/src/util/test-packet.c b/shared/n-dhcp4/src/util/test-packet.c new file mode 100644 index 000000000..0f5cba63a --- /dev/null +++ b/shared/n-dhcp4/src/util/test-packet.c @@ -0,0 +1,411 @@ +/* + * Packet Socket Tests + */ + +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include "n-dhcp4-private.h" +#include "link.h" +#include "netns.h" +#include "packet.h" +#include "test.h" + +typedef struct Blob { + uint16_t checksum; + uint8_t data[128]; +} Blob; + +static void test_checksum_one(Blob *blob, size_t size) { + uint16_t checksum; + + /* + * The only important property of the internet-checksum is that if the + * target blob is amended with its own checksum, the checksum + * calculation will become 0. So here we simply calculate the checksum + * with a dummy 0 in place, then put the checksum in and verify that + * the resulting checksum becomes 0. + */ + + blob->checksum = 0; + blob->checksum = packet_internet_checksum((uint8_t*)blob, size); + + checksum = packet_internet_checksum((uint8_t*)blob, size); + c_assert(!checksum); +} + +static void test_checksum_udp_one(Blob *blob, size_t size) { + uint16_t checksum; + + /* + * Like test_checksum_one(), here we calculate the target checksum, + * then place it in the source blob and calculate the checksum again. + * We expect it to be 0 in the end (i.e., pass the checksum test). + * + * Unlike the generic version, we must pass dummy UDP data into the + * helpers and also avoid a 0 checksum in the original source. + */ + + checksum = packet_internet_checksum_udp(&(struct in_addr){ htonl((10 << 24) | 2)}, + &(struct in_addr){ htonl((10 << 24) | 1)}, + 67, + 68, + blob->data, + sizeof(blob->data), + 0); + checksum = checksum ?: 0xffff; + checksum = packet_internet_checksum_udp(&(struct in_addr){ htonl((10 << 24) | 2)}, + &(struct in_addr){ htonl((10 << 24) | 1)}, + 67, + 68, + blob->data, + sizeof(blob->data), + checksum); + c_assert(!checksum); +} + +/* + * This generates some pseudo-random bytes and verifies that + * packet_internet_checksum{,_udp}() correctly calculates the checksum on this + * random-data. + */ +static void test_checksum(void) { + Blob blob = {}; + + /* fill @blob.data with some pseudo-random bytes */ + for (size_t i = 0; i < sizeof(blob.data); ++i) + blob.data[i] = i ^ (i >> 8) ^ (i >> 16) ^ (i >> 24); + + /* take chunks of @blob.data and verify their checksum */ + for (size_t j = 0; j < sizeof(uint64_t); ++j) { + for (uint32_t i = 0; i <= 0xffff; ++i) { + blob.data[0] = i & 0xff; + blob.data[1] = i >> 8; + test_checksum_one(&blob, sizeof(blob) - j); + test_checksum_udp_one(&blob, sizeof(blob) - j); + } + } +} + +static void test_new_packet_socket(Link *link, int *skp) { + struct sockaddr_ll addr = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_IP), + .sll_ifindex = link->ifindex, + }; + int r, on = 1; + + link_socket(link, skp, AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC); + + r = setsockopt(*skp, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on)); + c_assert(r >= 0); + + r = bind(*skp, (struct sockaddr*)&addr, sizeof(addr)); + c_assert(r >= 0); +} + +static void test_packet_unicast(int ifindex, int sk, void *buf, size_t n_buf, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst, + const struct ether_addr *haddr_dst) { + struct packet_sockaddr_ll addr = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_IP), + .sll_ifindex = ifindex, + .sll_halen = ETH_ALEN, + }; + size_t len; + int r; + + memcpy(addr.sll_addr, haddr_dst, ETH_ALEN); + + r = packet_sendto_udp(sk, buf, n_buf, &len, paddr_src, &addr, paddr_dst); + c_assert(!r); + c_assert(len == n_buf); +} + +static void test_packet_broadcast(int ifindex, int sk, void *buf, size_t n_buf, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst) { + struct packet_sockaddr_ll addr = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_IP), + .sll_ifindex = ifindex, + .sll_halen = ETH_ALEN, + }; + size_t len; + int r; + + memcpy(addr.sll_addr, (unsigned char[]){ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }, ETH_ALEN); + + r = packet_sendto_udp(sk, buf, n_buf, &len, paddr_src, &addr, paddr_dst); + c_assert(!r); + c_assert(len == n_buf); +} + +static void test_packet_packet(Link *link_src, + Link *link_dst, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst) { + _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1; + uint8_t buf[1024]; + size_t len; + int r; + + link_socket(link_src, &sk_src, AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC); + test_new_packet_socket(link_dst, &sk_dst); + + test_packet_unicast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst, &link_dst->mac); + test_packet_broadcast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst); + + r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len); + c_assert(!r); + c_assert(len == (ssize_t)sizeof(buf) - 1); + + r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len); + c_assert(!r); + c_assert(len == (ssize_t)sizeof(buf) - 1); +} + +static void test_packet_udp(Link *link_src, + Link *link_dst, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst) { + _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1; + uint8_t buf[1024]; + ssize_t len; + int r; + + link_socket(link_src, &sk_src, AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC); + link_socket(link_dst, &sk_dst, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC); + link_add_ip4(link_dst, &paddr_dst->sin_addr, 8); + + r = bind(sk_dst, (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(r >= 0); + + test_packet_unicast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst, &link_dst->mac); + test_packet_broadcast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst); + + len = recv(sk_dst, buf, sizeof(buf), 0); + c_assert(len == (ssize_t)sizeof(buf) - 1); + + len = recv(sk_dst, buf, sizeof(buf), 0); + c_assert(len == (ssize_t)sizeof(buf) - 1); + + link_del_ip4(link_dst, &paddr_dst->sin_addr, 8); +} + +static void test_udp_packet(Link *link_src, + Link *link_dst, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst) { + _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1; + uint8_t buf[1024]; + ssize_t slen; + size_t len; + int r; + + link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC); + test_new_packet_socket(link_dst, &sk_dst); + link_add_ip4(link_src, &paddr_src->sin_addr, 8); + link_add_ip4(link_dst, &paddr_dst->sin_addr, 8); + + slen = sendto(sk_src, buf, sizeof(buf) - 1, 0, + (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(slen == (ssize_t)sizeof(buf) - 1); + + r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len); + c_assert(!r); + c_assert(len == (ssize_t)sizeof(buf) - 1); + + link_del_ip4(link_dst, &paddr_dst->sin_addr, 8); + link_del_ip4(link_src, &paddr_src->sin_addr, 8); +} + +static void test_udp_udp(Link *link_src, + Link *link_dst, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst) { + _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1; + uint8_t buf[1024]; + ssize_t len; + int r; + + link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC); + link_socket(link_dst, &sk_dst, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC); + link_add_ip4(link_src, &paddr_src->sin_addr, 8); + link_add_ip4(link_dst, &paddr_dst->sin_addr, 8); + + r = bind(sk_dst, (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(r >= 0); + + len = sendto(sk_src, buf, sizeof(buf) - 1, 0, + (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(len == (ssize_t)sizeof(buf) - 1); + + len = recv(sk_dst, buf, sizeof(buf), 0); + c_assert(len == (ssize_t)sizeof(buf) - 1); + + link_del_ip4(link_dst, &paddr_dst->sin_addr, 8); + link_del_ip4(link_src, &paddr_src->sin_addr, 8); +} + +static void test_shutdown(Link *link_src, + Link *link_dst, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst) { + _c_cleanup_(c_closep) int sk_src = -1, sk_dst1 = -1, sk_dst2 = -1; + uint8_t buf[1024]; + ssize_t slen; + size_t len; + int r; + + link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC); + test_new_packet_socket(link_dst, &sk_dst1); + link_add_ip4(link_src, &paddr_src->sin_addr, 8); + link_add_ip4(link_dst, &paddr_dst->sin_addr, 8); + + /* 1 - send only to the packet socket */ + slen = sendto(sk_src, buf, sizeof(buf), 0, + (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(slen == (ssize_t)sizeof(buf)); + + /* create a UDP socket */ + link_socket(link_dst, &sk_dst2, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC); + + r = bind(sk_dst2, (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(r >= 0); + + /* 2 - send to both sockets */ + slen = sendto(sk_src, buf, sizeof(buf), 0, + (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(slen == (ssize_t)sizeof(buf)); + + /* shut down the packet socket */ + r = packet_shutdown(sk_dst1); + c_assert(r >= 0); + + /* 3 - send only to the UDP socket */ + slen = sendto(sk_src, buf, sizeof(buf), 0, + (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(slen == (ssize_t)sizeof(buf)); + + /* receive 1 and 2 on the packet socket */ + r = packet_recv_udp(sk_dst1, buf, sizeof(buf), &len); + c_assert(!r); + c_assert(len == (ssize_t)sizeof(buf)); + r = packet_recv_udp(sk_dst1, buf, sizeof(buf), &len); + c_assert(!r); + c_assert(len == (ssize_t)sizeof(buf)); + + /* make sure there is nothing more pending on the packet socket */ + slen = recv(sk_dst1, buf, sizeof(buf), MSG_DONTWAIT); + c_assert(slen < 0); + c_assert(errno == EAGAIN); + + /* receive 2 and 3 on the UDP socket */ + slen = recv(sk_dst2, buf, sizeof(buf), 0); + c_assert(slen == (ssize_t)sizeof(buf)); + slen = recv(sk_dst2, buf, sizeof(buf), 0); + c_assert(slen == (ssize_t)sizeof(buf)); + + /* make sure there is nothing more pending on the UDP socket */ + slen = recv(sk_dst1, buf, sizeof(buf), MSG_DONTWAIT); + c_assert(slen < 0); + c_assert(errno == EAGAIN); + + link_del_ip4(link_dst, &paddr_dst->sin_addr, 8); + link_del_ip4(link_src, &paddr_src->sin_addr, 8); +} + +static void test_ip_hdr(Link *link_src, + Link *link_dst, + const struct sockaddr_in *paddr_src, + const struct sockaddr_in *paddr_dst) { + _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1; + uint8_t ipopts[5] = { 1, 1, 1, 1, 1 }; + uint8_t buf[1024]; + ssize_t slen; + size_t len; + int r; + + /* + * This test sends a packet from a UDP socket to a packet socket, but + * appends 5-bytes of IPOPT_NOOP ip-options. With this we verify our + * packet socket correctly skips additional ip-options and does not + * interpret the ip-header as a fixed size header. + */ + + link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC); + test_new_packet_socket(link_dst, &sk_dst); + link_add_ip4(link_src, &paddr_src->sin_addr, 8); + link_add_ip4(link_dst, &paddr_dst->sin_addr, 8); + + r = setsockopt(sk_src, IPPROTO_IP, IP_OPTIONS, ipopts, sizeof(ipopts)); + c_assert(r >= 0); + + slen = sendto(sk_src, buf, sizeof(buf) - 1, 0, + (struct sockaddr*)paddr_dst, sizeof(*paddr_dst)); + c_assert(slen == (ssize_t)sizeof(buf) - 1); + + r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len); + c_assert(!r); + c_assert(len == (ssize_t)sizeof(buf) - 1); + + link_del_ip4(link_dst, &paddr_dst->sin_addr, 8); + link_del_ip4(link_src, &paddr_src->sin_addr, 8); +} + +/* + * This test verifies that we can send packets from/to packet/udp sockets. It + * tests all combinations: packet->packet, packet->udp, udp->packet, udp->udp + * + * Furthermore, this test checks for some of the behavioural properties of our + * packet socket helpers. + */ +static void test_packet(void) { + _c_cleanup_(netns_closep) int ns_src = -1, ns_dst = -1; + _c_cleanup_(link_deinit) Link link_src = LINK_NULL(link_src); + _c_cleanup_(link_deinit) Link link_dst = LINK_NULL(link_dst); + struct sockaddr_in paddr_src = { + .sin_family = AF_INET, + .sin_addr = (struct in_addr){ htonl(10<<24 | 1) }, + .sin_port = htons(10), + }; + struct sockaddr_in paddr_dst = { + .sin_family = AF_INET, + .sin_addr = (struct in_addr){ htonl(10<<24 | 2) }, + .sin_port = htons(11), + }; + + /* setup */ + + netns_new(&ns_src); + netns_new(&ns_dst); + link_new_veth(&link_src, &link_dst, ns_src, ns_dst); + + /* communication tests */ + + test_packet_packet(&link_src, &link_dst, &paddr_src, &paddr_dst); + test_packet_udp(&link_src, &link_dst, &paddr_src, &paddr_dst); + test_udp_packet(&link_src, &link_dst, &paddr_src, &paddr_dst); + test_udp_udp(&link_src, &link_dst, &paddr_src, &paddr_dst); + + /* behavior tests */ + + test_shutdown(&link_src, &link_dst, &paddr_src, &paddr_dst); + test_ip_hdr(&link_src, &link_dst, &paddr_src, &paddr_dst); +} + +int main(int argc, char **argv) { + test_setup(); + + test_checksum(); + test_packet(); + + return 0; +} diff --git a/shared/n-dhcp4/subprojects/c-list b/shared/n-dhcp4/subprojects/c-list new file mode 160000 index 000000000..2e4b605c6 --- /dev/null +++ b/shared/n-dhcp4/subprojects/c-list @@ -0,0 +1 @@ +Subproject commit 2e4b605c6217cd3c8a1ef773f82f5cc329ba650d diff --git a/shared/n-dhcp4/subprojects/c-siphash b/shared/n-dhcp4/subprojects/c-siphash new file mode 160000 index 000000000..7c42c5925 --- /dev/null +++ b/shared/n-dhcp4/subprojects/c-siphash @@ -0,0 +1 @@ +Subproject commit 7c42c592581906fef19458372b8db2b643278211 diff --git a/shared/n-dhcp4/subprojects/c-stdaux b/shared/n-dhcp4/subprojects/c-stdaux new file mode 160000 index 000000000..11930d259 --- /dev/null +++ b/shared/n-dhcp4/subprojects/c-stdaux @@ -0,0 +1 @@ +Subproject commit 11930d259212605a15430523472ef54e0c7654ee