source: mainline/uspace/srv/net/dhcp/dhcp.c@ bb4ba92

ticket/834-toolchain-update topic/msim-upgrade topic/simplify-dev-export
Last change on this file since bb4ba92 was b7155d7, checked in by Jiri Svoboda <jiri@…>, 3 years ago

DHCP should specify a parameter request list with the requested fields

Specifying that we need a DNS server makes DHCP work correctly in
VirtualBox. If not specified, VirtualBox provides an incorrect
DNS server address.

  • Property mode set to 100644
File size: 15.8 KB
Line 
1/*
2 * Copyright (c) 2022 Jiri Svoboda
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * - Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * - Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * - The name of the author may not be used to endorse or promote products
15 * derived from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29/** @addtogroup dhcp
30 * @{
31 */
32/**
33 * @file
34 * @brief DHCP client
35 */
36
37#include <adt/list.h>
38#include <bitops.h>
39#include <byteorder.h>
40#include <errno.h>
41#include <str_error.h>
42#include <fibril_synch.h>
43#include <inet/addr.h>
44#include <inet/eth_addr.h>
45#include <inet/dnsr.h>
46#include <inet/inetcfg.h>
47#include <io/log.h>
48#include <loc.h>
49#include <rndgen.h>
50#include <stdio.h>
51#include <stdlib.h>
52#include <str.h>
53
54#include "dhcp.h"
55#include "dhcp_std.h"
56#include "transport.h"
57
58enum {
59 /** In microseconds */
60 dhcp_discover_timeout_val = 5 * 1000 * 1000,
61 /** In microseconds */
62 dhcp_request_timeout_val = 1 * 1000 * 1000,
63 dhcp_discover_retries = 5,
64 dhcp_request_retries = 3
65};
66
67#define MAX_MSG_SIZE 1024
68static uint8_t msgbuf[MAX_MSG_SIZE];
69
70/** List of registered links (of dhcp_link_t) */
71static list_t dhcp_links;
72
73static void dhcpsrv_discover_timeout(void *);
74static void dhcpsrv_request_timeout(void *);
75
76typedef enum {
77 ds_bound,
78 ds_fail,
79 ds_init,
80 ds_init_reboot,
81 ds_rebinding,
82 ds_renewing,
83 ds_requesting,
84 ds_selecting
85} dhcp_state_t;
86
87typedef struct {
88 /** Message type */
89 enum dhcp_msg_type msg_type;
90 /** Offered address */
91 inet_naddr_t oaddr;
92 /** Server address */
93 inet_addr_t srv_addr;
94 /** Router address */
95 inet_addr_t router;
96 /** DNS server */
97 inet_addr_t dns_server;
98 /** Transaction ID */
99 uint32_t xid;
100} dhcp_offer_t;
101
102typedef struct {
103 /** Link to dhcp_links list */
104 link_t links;
105 /** Link service ID */
106 service_id_t link_id;
107 /** Link info */
108 inet_link_info_t link_info;
109 /** Transport */
110 dhcp_transport_t dt;
111 /** Transport timeout */
112 fibril_timer_t *timeout;
113 /** Number of retries */
114 int retries_left;
115 /** Link state */
116 dhcp_state_t state;
117 /** Last received offer */
118 dhcp_offer_t offer;
119 /** Random number generator */
120 rndgen_t *rndgen;
121} dhcp_link_t;
122
123static void dhcpsrv_recv(void *, void *, size_t);
124
125/** Decode subnet mask into subnet prefix length. */
126static errno_t subnet_mask_decode(uint32_t mask, int *bits)
127{
128 int zbits;
129 uint32_t nmask;
130
131 if (mask == 0xffffffff) {
132 *bits = 32;
133 return EOK;
134 }
135
136 zbits = 1 + fnzb32(mask ^ 0xffffffff);
137 nmask = BIT_RRANGE(uint32_t, zbits);
138
139 if ((mask ^ nmask) != 0xffffffff) {
140 /* The mask is not in the form 1**n,0**m */
141 return EINVAL;
142 }
143
144 *bits = 32 - zbits;
145 return EOK;
146}
147
148static uint32_t dhcp_uint32_decode(uint8_t *data)
149{
150 return
151 ((uint32_t)data[0] << 24) |
152 ((uint32_t)data[1] << 16) |
153 ((uint32_t)data[2] << 8) |
154 ((uint32_t)data[3]);
155}
156
157static errno_t dhcp_send_discover(dhcp_link_t *dlink)
158{
159 dhcp_hdr_t *hdr = (dhcp_hdr_t *)msgbuf;
160 uint8_t *opt = msgbuf + sizeof(dhcp_hdr_t);
161 uint32_t xid;
162 errno_t rc;
163 size_t i;
164
165 rc = rndgen_uint32(dlink->rndgen, &xid);
166 if (rc != EOK)
167 return rc;
168
169 memset(msgbuf, 0, MAX_MSG_SIZE);
170 hdr->op = op_bootrequest;
171 hdr->htype = 1; /* AHRD_ETHERNET */
172 hdr->hlen = ETH_ADDR_SIZE;
173 hdr->xid = host2uint32_t_be(xid);
174 hdr->flags = host2uint16_t_be(flag_broadcast);
175
176 eth_addr_encode(&dlink->link_info.mac_addr, hdr->chaddr);
177 hdr->opt_magic = host2uint32_t_be(dhcp_opt_magic);
178
179 i = 0;
180
181 opt[i++] = opt_msg_type;
182 opt[i++] = 1;
183 opt[i++] = msg_dhcpdiscover;
184
185 opt[i++] = opt_param_req_list;
186 opt[i++] = 3;
187 opt[i++] = 1; /* subnet mask */
188 opt[i++] = 6; /* DNS server */
189 opt[i++] = 3; /* router */
190
191 opt[i++] = opt_end;
192
193 return dhcp_send(&dlink->dt, msgbuf, sizeof(dhcp_hdr_t) + i);
194}
195
196static errno_t dhcp_send_request(dhcp_link_t *dlink, dhcp_offer_t *offer)
197{
198 dhcp_hdr_t *hdr = (dhcp_hdr_t *)msgbuf;
199 uint8_t *opt = msgbuf + sizeof(dhcp_hdr_t);
200 size_t i;
201
202 memset(msgbuf, 0, MAX_MSG_SIZE);
203 hdr->op = op_bootrequest;
204 hdr->htype = 1; /* AHRD_ETHERNET */
205 hdr->hlen = 6;
206 hdr->xid = host2uint32_t_be(offer->xid);
207 hdr->flags = host2uint16_t_be(flag_broadcast);
208 eth_addr_encode(&dlink->link_info.mac_addr, hdr->chaddr);
209 hdr->opt_magic = host2uint32_t_be(dhcp_opt_magic);
210
211 i = 0;
212
213 opt[i++] = opt_msg_type;
214 opt[i++] = 1;
215 opt[i++] = msg_dhcprequest;
216
217 opt[i++] = opt_req_ip_addr;
218 opt[i++] = 4;
219 opt[i++] = offer->oaddr.addr >> 24;
220 opt[i++] = (offer->oaddr.addr >> 16) & 0xff;
221 opt[i++] = (offer->oaddr.addr >> 8) & 0xff;
222 opt[i++] = offer->oaddr.addr & 0xff;
223
224 opt[i++] = opt_server_id;
225 opt[i++] = 4;
226 opt[i++] = offer->srv_addr.addr >> 24;
227 opt[i++] = (offer->srv_addr.addr >> 16) & 0xff;
228 opt[i++] = (offer->srv_addr.addr >> 8) & 0xff;
229 opt[i++] = offer->srv_addr.addr & 0xff;
230
231 opt[i++] = opt_end;
232
233 return dhcp_send(&dlink->dt, msgbuf, sizeof(dhcp_hdr_t) + i);
234}
235
236static errno_t dhcp_parse_reply(void *msg, size_t size, dhcp_offer_t *offer)
237{
238 dhcp_hdr_t *hdr = (dhcp_hdr_t *)msg;
239 inet_addr_t yiaddr;
240 inet_addr_t siaddr;
241 inet_addr_t giaddr;
242 uint32_t subnet_mask;
243 bool have_subnet_mask = false;
244 bool have_server_id = false;
245 int subnet_bits;
246 char *saddr;
247 uint8_t opt_type, opt_len;
248 uint8_t *msgb;
249 errno_t rc;
250 size_t i;
251
252 log_msg(LOG_DEFAULT, LVL_DEBUG, "Receive reply");
253 memset(offer, 0, sizeof(*offer));
254
255 inet_addr_set(uint32_t_be2host(hdr->yiaddr), &yiaddr);
256 rc = inet_addr_format(&yiaddr, &saddr);
257 if (rc != EOK)
258 return rc;
259
260 log_msg(LOG_DEFAULT, LVL_DEBUG, "Your IP address: %s", saddr);
261 free(saddr);
262
263 inet_addr_set(uint32_t_be2host(hdr->siaddr), &siaddr);
264 rc = inet_addr_format(&siaddr, &saddr);
265 if (rc != EOK)
266 return rc;
267
268 log_msg(LOG_DEFAULT, LVL_DEBUG, "Next server IP address: %s", saddr);
269 free(saddr);
270
271 inet_addr_set(uint32_t_be2host(hdr->giaddr), &giaddr);
272 rc = inet_addr_format(&giaddr, &saddr);
273 if (rc != EOK)
274 return rc;
275
276 log_msg(LOG_DEFAULT, LVL_DEBUG, "Relay agent IP address: %s", saddr);
277 free(saddr);
278
279 inet_naddr_set(yiaddr.addr, 0, &offer->oaddr);
280 offer->xid = uint32_t_be2host(hdr->xid);
281
282 msgb = (uint8_t *)msg;
283
284 i = sizeof(dhcp_hdr_t);
285 while (i < size) {
286 opt_type = msgb[i++];
287
288 if (opt_type == opt_pad)
289 continue;
290 if (opt_type == opt_end)
291 break;
292
293 if (i >= size)
294 return EINVAL;
295
296 opt_len = msgb[i++];
297
298 if (i + opt_len > size)
299 return EINVAL;
300
301 switch (opt_type) {
302 case opt_subnet_mask:
303 if (opt_len != 4)
304 return EINVAL;
305 subnet_mask = dhcp_uint32_decode(&msgb[i]);
306 rc = subnet_mask_decode(subnet_mask, &subnet_bits);
307 if (rc != EOK)
308 return EINVAL;
309 offer->oaddr.prefix = subnet_bits;
310 have_subnet_mask = true;
311 break;
312 case opt_msg_type:
313 if (opt_len != 1)
314 return EINVAL;
315 offer->msg_type = msgb[i];
316 break;
317 case opt_server_id:
318 if (opt_len != 4)
319 return EINVAL;
320 inet_addr_set(dhcp_uint32_decode(&msgb[i]),
321 &offer->srv_addr);
322 have_server_id = true;
323 break;
324 case opt_router:
325 if (opt_len != 4)
326 return EINVAL;
327 inet_addr_set(dhcp_uint32_decode(&msgb[i]),
328 &offer->router);
329 break;
330 case opt_dns_server:
331 if (opt_len < 4 || opt_len % 4 != 0)
332 return EINVAL;
333 /* XXX Handle multiple DNS servers properly */
334 inet_addr_set(dhcp_uint32_decode(&msgb[i]),
335 &offer->dns_server);
336 break;
337 case opt_end:
338 break;
339 default:
340 break;
341 }
342
343 /* Advance to the next option */
344 i = i + opt_len;
345 }
346
347 if (!have_server_id) {
348 log_msg(LOG_DEFAULT, LVL_ERROR, "Missing server ID option.");
349 return rc;
350 }
351
352 if (!have_subnet_mask) {
353 log_msg(LOG_DEFAULT, LVL_ERROR, "Missing subnet mask option.");
354 return rc;
355 }
356
357 rc = inet_naddr_format(&offer->oaddr, &saddr);
358 if (rc != EOK)
359 return rc;
360
361 log_msg(LOG_DEFAULT, LVL_DEBUG, "Offered network address: %s", saddr);
362 free(saddr);
363
364 if (offer->router.addr != 0) {
365 rc = inet_addr_format(&offer->router, &saddr);
366 if (rc != EOK)
367 return rc;
368
369 log_msg(LOG_DEFAULT, LVL_DEBUG, "Router address: %s", saddr);
370 free(saddr);
371 }
372
373 if (offer->dns_server.addr != 0) {
374 rc = inet_addr_format(&offer->dns_server, &saddr);
375 if (rc != EOK)
376 return rc;
377
378 log_msg(LOG_DEFAULT, LVL_DEBUG, "DNS server: %s", saddr);
379 free(saddr);
380 }
381
382 return EOK;
383}
384
385static errno_t dhcp_cfg_create(service_id_t iplink, dhcp_offer_t *offer)
386{
387 errno_t rc;
388 service_id_t addr_id;
389 service_id_t sroute_id;
390 inet_naddr_t defr;
391
392 rc = inetcfg_addr_create_static("dhcp4a", &offer->oaddr, iplink,
393 &addr_id);
394 if (rc != EOK) {
395 log_msg(LOG_DEFAULT, LVL_ERROR,
396 "Error creating IP address %s: %s", "dhcp4a", str_error(rc));
397 return rc;
398 }
399
400 if (offer->router.addr != 0) {
401 inet_naddr_set(0, 0, &defr);
402
403 rc = inetcfg_sroute_create("dhcpdef", &defr, &offer->router, &sroute_id);
404 if (rc != EOK) {
405 log_msg(LOG_DEFAULT, LVL_ERROR, "Error creating "
406 "default route %s: %s.", "dhcpdef", str_error(rc));
407 return rc;
408 }
409 }
410
411 if (offer->dns_server.addr != 0) {
412 rc = dnsr_set_srvaddr(&offer->dns_server);
413 if (rc != EOK) {
414 log_msg(LOG_DEFAULT, LVL_ERROR, "Error setting "
415 "nameserver address: %s)", str_error(rc));
416 return rc;
417 }
418 }
419
420 return EOK;
421}
422
423void dhcpsrv_links_init(void)
424{
425 list_initialize(&dhcp_links);
426}
427
428static dhcp_link_t *dhcpsrv_link_find(service_id_t link_id)
429{
430 list_foreach(dhcp_links, links, dhcp_link_t, dlink) {
431 if (dlink->link_id == link_id)
432 return dlink;
433 }
434
435 return NULL;
436}
437
438static void dhcp_link_set_failed(dhcp_link_t *dlink)
439{
440 log_msg(LOG_DEFAULT, LVL_NOTE, "Giving up on link %s",
441 dlink->link_info.name);
442 dlink->state = ds_fail;
443}
444
445static errno_t dhcp_discover_proc(dhcp_link_t *dlink)
446{
447 dlink->state = ds_selecting;
448
449 errno_t rc = dhcp_send_discover(dlink);
450 if (rc != EOK)
451 return EIO;
452
453 dlink->retries_left = dhcp_discover_retries;
454
455 if ((dlink->timeout->state == fts_not_set) ||
456 (dlink->timeout->state == fts_fired))
457 fibril_timer_set(dlink->timeout, dhcp_discover_timeout_val,
458 dhcpsrv_discover_timeout, dlink);
459
460 return rc;
461}
462
463errno_t dhcpsrv_link_add(service_id_t link_id)
464{
465 dhcp_link_t *dlink;
466 errno_t rc;
467
468 log_msg(LOG_DEFAULT, LVL_DEBUG, "dhcpsrv_link_add(%zu)", link_id);
469
470 if (dhcpsrv_link_find(link_id) != NULL) {
471 log_msg(LOG_DEFAULT, LVL_NOTE, "Link %zu already added",
472 link_id);
473 return EEXIST;
474 }
475
476 dlink = calloc(1, sizeof(dhcp_link_t));
477 if (dlink == NULL)
478 return ENOMEM;
479
480 rc = rndgen_create(&dlink->rndgen);
481 if (rc != EOK)
482 goto error;
483
484 dlink->link_id = link_id;
485 dlink->timeout = fibril_timer_create(NULL);
486 if (dlink->timeout == NULL) {
487 rc = ENOMEM;
488 goto error;
489 }
490
491 /* Get link hardware address */
492 rc = inetcfg_link_get(link_id, &dlink->link_info);
493 if (rc != EOK) {
494 log_msg(LOG_DEFAULT, LVL_ERROR, "Error getting properties "
495 "for link %zu.", link_id);
496 rc = EIO;
497 goto error;
498 }
499
500 rc = dhcp_transport_init(&dlink->dt, link_id, dhcpsrv_recv, dlink);
501 if (rc != EOK) {
502 log_msg(LOG_DEFAULT, LVL_ERROR, "Error initializing DHCP "
503 "transport for link %s.", dlink->link_info.name);
504 rc = EIO;
505 goto error;
506 }
507
508 log_msg(LOG_DEFAULT, LVL_DEBUG, "Send DHCPDISCOVER");
509 rc = dhcp_discover_proc(dlink);
510 if (rc != EOK) {
511 log_msg(LOG_DEFAULT, LVL_ERROR, "Error sending DHCPDISCOVER.");
512 dhcp_link_set_failed(dlink);
513 rc = EIO;
514 goto error;
515 }
516
517 list_append(&dlink->links, &dhcp_links);
518
519 return EOK;
520error:
521 if (dlink != NULL && dlink->rndgen != NULL)
522 rndgen_destroy(dlink->rndgen);
523 if (dlink != NULL && dlink->timeout != NULL)
524 fibril_timer_destroy(dlink->timeout);
525 free(dlink);
526 return rc;
527}
528
529errno_t dhcpsrv_link_remove(service_id_t link_id)
530{
531 return ENOTSUP;
532}
533
534errno_t dhcpsrv_discover(service_id_t link_id)
535{
536 log_msg(LOG_DEFAULT, LVL_DEBUG, "dhcpsrv_link_add(%zu)", link_id);
537
538 dhcp_link_t *dlink = dhcpsrv_link_find(link_id);
539
540 if (dlink == NULL) {
541 log_msg(LOG_DEFAULT, LVL_NOTE, "Link %zu doesn't exist",
542 link_id);
543 return EINVAL;
544 }
545
546 return dhcp_discover_proc(dlink);
547}
548
549static void dhcpsrv_recv_offer(dhcp_link_t *dlink, dhcp_offer_t *offer)
550{
551 errno_t rc;
552
553 if (dlink->state != ds_selecting) {
554 log_msg(LOG_DEFAULT, LVL_DEBUG, "Received offer in state "
555 " %d, ignoring.", (int)dlink->state);
556 return;
557 }
558
559 fibril_timer_clear(dlink->timeout);
560 dlink->offer = *offer;
561 dlink->state = ds_requesting;
562
563 log_msg(LOG_DEFAULT, LVL_DEBUG, "Send DHCPREQUEST");
564 rc = dhcp_send_request(dlink, offer);
565 if (rc != EOK) {
566 log_msg(LOG_DEFAULT, LVL_DEBUG, "Error sending request.");
567 return;
568 }
569
570 dlink->retries_left = dhcp_request_retries;
571 fibril_timer_set(dlink->timeout, dhcp_request_timeout_val,
572 dhcpsrv_request_timeout, dlink);
573}
574
575static void dhcpsrv_recv_ack(dhcp_link_t *dlink, dhcp_offer_t *offer)
576{
577 errno_t rc;
578
579 if (dlink->state != ds_requesting) {
580 log_msg(LOG_DEFAULT, LVL_DEBUG, "Received ack in state "
581 " %d, ignoring.", (int)dlink->state);
582 return;
583 }
584
585 fibril_timer_clear(dlink->timeout);
586 dlink->offer = *offer;
587 dlink->state = ds_bound;
588
589 rc = dhcp_cfg_create(dlink->link_id, offer);
590 if (rc != EOK) {
591 log_msg(LOG_DEFAULT, LVL_DEBUG, "Error creating configuration.");
592 return;
593 }
594
595 log_msg(LOG_DEFAULT, LVL_NOTE, "%s: Successfully configured.",
596 dlink->link_info.name);
597}
598
599static void dhcpsrv_recv(void *arg, void *msg, size_t size)
600{
601 dhcp_link_t *dlink = (dhcp_link_t *)arg;
602 dhcp_offer_t offer;
603 errno_t rc;
604
605 log_msg(LOG_DEFAULT, LVL_DEBUG, "%s: dhcpsrv_recv() %zu bytes",
606 dlink->link_info.name, size);
607
608 rc = dhcp_parse_reply(msg, size, &offer);
609 if (rc != EOK) {
610 log_msg(LOG_DEFAULT, LVL_DEBUG, "Error parsing reply");
611 return;
612 }
613
614 switch (offer.msg_type) {
615 case msg_dhcpoffer:
616 dhcpsrv_recv_offer(dlink, &offer);
617 break;
618 case msg_dhcpack:
619 dhcpsrv_recv_ack(dlink, &offer);
620 break;
621 default:
622 log_msg(LOG_DEFAULT, LVL_DEBUG, "Received unexpected "
623 "message type. %d", (int)offer.msg_type);
624 break;
625 }
626}
627
628static void dhcpsrv_discover_timeout(void *arg)
629{
630 dhcp_link_t *dlink = (dhcp_link_t *)arg;
631 errno_t rc;
632
633 assert(dlink->state == ds_selecting);
634 log_msg(LOG_DEFAULT, LVL_NOTE, "%s: dhcpsrv_discover_timeout",
635 dlink->link_info.name);
636
637 if (dlink->retries_left == 0) {
638 log_msg(LOG_DEFAULT, LVL_NOTE, "Retries exhausted");
639 dhcp_link_set_failed(dlink);
640 return;
641 }
642 --dlink->retries_left;
643
644 log_msg(LOG_DEFAULT, LVL_DEBUG, "Send DHCPDISCOVER");
645 rc = dhcp_send_discover(dlink);
646 if (rc != EOK) {
647 log_msg(LOG_DEFAULT, LVL_ERROR, "Error sending DHCPDISCOVER");
648 dhcp_link_set_failed(dlink);
649 return;
650 }
651
652 fibril_timer_set(dlink->timeout, dhcp_discover_timeout_val,
653 dhcpsrv_discover_timeout, dlink);
654}
655
656static void dhcpsrv_request_timeout(void *arg)
657{
658 dhcp_link_t *dlink = (dhcp_link_t *)arg;
659 errno_t rc;
660
661 assert(dlink->state == ds_requesting);
662 log_msg(LOG_DEFAULT, LVL_NOTE, "%s: dhcpsrv_request_timeout",
663 dlink->link_info.name);
664
665 if (dlink->retries_left == 0) {
666 log_msg(LOG_DEFAULT, LVL_NOTE, "Retries exhausted");
667 dhcp_link_set_failed(dlink);
668 return;
669 }
670 --dlink->retries_left;
671
672 log_msg(LOG_DEFAULT, LVL_DEBUG, "Send DHCPREQUEST");
673 rc = dhcp_send_request(dlink, &dlink->offer);
674 if (rc != EOK) {
675 log_msg(LOG_DEFAULT, LVL_DEBUG, "Error sending request.");
676 dhcp_link_set_failed(dlink);
677 return;
678 }
679
680 fibril_timer_set(dlink->timeout, dhcp_request_timeout_val,
681 dhcpsrv_request_timeout, dlink);
682}
683
684/** @}
685 */
Note: See TracBrowser for help on using the repository browser.