NetworkManager/shared/n-dhcp4/src/test-run-client.c
Tom Gundersen e43b1791a3 Merge commit 'e23b3c9c3ac86b065eef002fa5c4321cc4a87df2' as 'shared/n-dhcp4'
Imported n-dhcp4 code with command:

  git subtree add --prefix shared/n-dhcp4/ git@github.com:nettools/n-dhcp4.git master --squash

To update the library use:

  git subtree pull --prefix shared/n-dhcp4/ git@github.com:nettools/n-dhcp4.git master --squash
2019-05-25 02:02:04 +02:00

713 lines
24 KiB
C

/*
* 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 <assert.h>
#include <c-stdaux.h>
#include <errno.h>
#include <getopt.h>
#include <net/if.h>
#include <netinet/ether.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}