summaryrefslogtreecommitdiff
path: root/shared/n-dhcp4/src/n-dhcp4-client.c
blob: 72f4ee04e95cc8ee367aa1d6599f6569ae7401ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
/*
 * 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 <assert.h>
#include <c-list.h>
#include <c-stdaux.h>
#include <errno.h>
#include <linux/if_ether.h>
#include <linux/if_infiniband.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
#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;
        dup->log.level = config->log.level;
        dup->log.func = config->log.func;
        dup->log.data = config->log.data;

        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;

        if (n_mac > sizeof(config->mac))
                n_mac = sizeof(config->mac);

        memcpy(config->mac, mac, n_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;

        if (n_mac > sizeof(config->mac))
                n_mac = sizeof(config->mac);

        memcpy(config->broadcast_mac, mac, n_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;
}

_c_public_ void n_dhcp4_client_config_set_log_level(NDhcp4ClientConfig *config, int level) {
        config->log.level = level;
}

_c_public_ void n_dhcp4_client_config_set_log_func(NDhcp4ClientConfig *config, NDhcp4LogFunc func, void *data) {
        config->log.func = func;
        config->log.data = data;
}

/**
 * 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) {
                if (errno != EINVAL)
                        return -errno;
                client->fd_timer = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
                if (client->fd_timer < 0)
                        return -errno;
                client->timer_is_monotonic = true;
        }

        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) {
                uint64_t scheduled_timeout = timeout;
                int flags = TFD_TIMER_ABSTIME;

                if (   timeout != 0
                    && client->timer_is_monotonic) {
                        uint64_t now;

                        /* the timerfd ticks with CLOCK_MONOTONIC. Calculate and set the relative
                         * timeout. */
                        now = n_dhcp4_gettime(CLOCK_BOOTTIME);
                        if (timeout <= now)
                                timeout = 1;
                        else
                                timeout = timeout - now;
                        flags = 0;
                }
                r = timerfd_settime(client->fd_timer,
                                    flags,
                                    &(struct itimerspec){
                                        .it_value = {
                                                .tv_sec = timeout / UINT64_C(1000000000),
                                                .tv_nsec = timeout % UINT64_C(1000000000),
                                        },
                                    },
                                    NULL);
                c_assert(r >= 0);

                client->scheduled_timeout = scheduled_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;
}