]>
Commit | Line | Data |
---|---|---|
31b3e1bb MM |
1 | /* |
2 | * BIRD Internet Routing Daemon -- Configuration File Handling | |
3 | * | |
50fe90ed | 4 | * (c) 1998--2000 Martin Mares <mj@ucw.cz> |
31b3e1bb MM |
5 | * |
6 | * Can be freely distributed and used under the terms of the GNU GPL. | |
7 | */ | |
8 | ||
06607335 MM |
9 | /** |
10 | * DOC: Configuration manager | |
11 | * | |
725270cb | 12 | * Configuration of BIRD is complex, yet straightforward. There are three |
06607335 | 13 | * modules taking care of the configuration: config manager (which takes care |
58f7d004 | 14 | * of storage of the config information and controls switching between configs), |
2e9b2421 | 15 | * lexical analyzer and parser. |
06607335 MM |
16 | * |
17 | * The configuration manager stores each config as a &config structure | |
18 | * accompanied by a linear pool from which all information associated | |
19 | * with the config and pointed to by the &config structure is allocated. | |
20 | * | |
725270cb | 21 | * There can exist up to four different configurations at one time: an active |
06607335 | 22 | * one (pointed to by @config), configuration we are just switching from |
9b9a7143 OZ |
23 | * (@old_config), one queued for the next reconfiguration (@future_config; if |
24 | * there is one and the user wants to reconfigure once again, we just free the | |
25 | * previous queued config and replace it with the new one) and finally a config | |
26 | * being parsed (@new_config). The stored @old_config is also used for undo | |
27 | * reconfiguration, which works in a similar way. Reconfiguration could also | |
28 | * have timeout (using @config_timer) and undo is automatically called if the | |
29 | * new configuration is not confirmed later. The new config (@new_config) and | |
30 | * associated linear pool (@cfg_mem) is non-NULL only during parsing. | |
06607335 | 31 | * |
9b9a7143 OZ |
32 | * Loading of new configuration is very simple: just call config_alloc() to get |
33 | * a new &config structure, then use config_parse() to parse a configuration | |
34 | * file and fill all fields of the structure and finally ask the config manager | |
35 | * to switch to the new config by calling config_commit(). | |
06607335 MM |
36 | * |
37 | * CLI commands are parsed in a very similar way -- there is also a stripped-down | |
2e9b2421 | 38 | * &config structure associated with them and they are lex-ed and parsed by the |
06607335 MM |
39 | * same functions, only a special fake token is prepended before the command |
40 | * text to make the parser recognize only the rules corresponding to CLI commands. | |
41 | */ | |
42 | ||
31b3e1bb MM |
43 | #include <setjmp.h> |
44 | #include <stdarg.h> | |
45 | ||
6b9fa320 | 46 | #undef LOCAL_DEBUG |
50fe90ed | 47 | |
31b3e1bb | 48 | #include "nest/bird.h" |
0e02abfd | 49 | #include "nest/route.h" |
31b3e1bb MM |
50 | #include "nest/protocol.h" |
51 | #include "nest/iface.h" | |
52 | #include "lib/resource.h" | |
53 | #include "lib/string.h" | |
50fe90ed | 54 | #include "lib/event.h" |
43270902 | 55 | #include "lib/timer.h" |
31b3e1bb MM |
56 | #include "conf/conf.h" |
57 | #include "filter/filter.h" | |
58 | ||
59 | static jmp_buf conf_jmpbuf; | |
60 | ||
a92cf57d OZ |
61 | struct config *config, *new_config; |
62 | ||
63 | static struct config *old_config; /* Old configuration */ | |
64 | static struct config *future_config; /* New config held here if recon requested during recon */ | |
65 | static int old_cftype; /* Type of transition old_config -> config (RECONFIG_SOFT/HARD) */ | |
66 | static int future_cftype; /* Type of scheduled transition, may also be RECONFIG_UNDO */ | |
67 | /* Note that when future_cftype is RECONFIG_UNDO, then future_config is NULL, | |
68 | therefore proper check for future scheduled config checks future_cftype */ | |
69 | ||
70 | static event *config_event; /* Event for finalizing reconfiguration */ | |
71 | static timer *config_timer; /* Timer for scheduled configuration rollback */ | |
72 | ||
73 | /* These are public just for cmd_show_status(), should not be accessed elsewhere */ | |
74 | int shutting_down; /* Shutdown requested, do not accept new config changes */ | |
75 | int configuring; /* Reconfiguration is running */ | |
76 | int undo_available; /* Undo was not requested from last reconfiguration */ | |
77 | /* Note that both shutting_down and undo_available are related to requests, not processing */ | |
31b3e1bb | 78 | |
06607335 MM |
79 | /** |
80 | * config_alloc - allocate a new configuration | |
81 | * @name: name of the config | |
82 | * | |
83 | * This function creates new &config structure, attaches a resource | |
84 | * pool and a linear memory pool to it and makes it available for | |
85 | * further use. Returns a pointer to the structure. | |
86 | */ | |
31b3e1bb MM |
87 | struct config * |
88 | config_alloc(byte *name) | |
89 | { | |
90 | pool *p = rp_new(&root_pool, "Config"); | |
d4ff7482 | 91 | linpool *l = lp_new(p, 4080); |
31b3e1bb MM |
92 | struct config *c = lp_allocz(l, sizeof(struct config)); |
93 | ||
9b9a7143 OZ |
94 | /* Duplication of name string in local linear pool */ |
95 | uint nlen = strlen(name) + 1; | |
96 | char *ndup = lp_allocu(l, nlen); | |
97 | memcpy(ndup, name, nlen); | |
98 | ||
cf31112f | 99 | c->mrtdump_file = -1; /* Hack, this should be sysdep-specific */ |
31b3e1bb | 100 | c->pool = p; |
9b9a7143 OZ |
101 | c->mem = l; |
102 | c->file_name = ndup; | |
43270902 | 103 | c->load_time = now; |
90eb5e7a OZ |
104 | c->tf_route = c->tf_proto = (struct timeformat){"%T", "%F", 20*3600}; |
105 | c->tf_base = c->tf_log = (struct timeformat){"%F %T", NULL, 0}; | |
0c791f87 | 106 | c->gr_wait = DEFAULT_GR_WAIT; |
c37e7851 | 107 | |
31b3e1bb MM |
108 | return c; |
109 | } | |
110 | ||
06607335 MM |
111 | /** |
112 | * config_parse - parse a configuration | |
113 | * @c: configuration | |
114 | * | |
115 | * config_parse() reads input by calling a hook function pointed to | |
116 | * by @cf_read_hook and parses it according to the configuration | |
117 | * grammar. It also calls all the preconfig and postconfig hooks | |
2e9b2421 | 118 | * before, resp. after parsing. |
06607335 MM |
119 | * |
120 | * Result: 1 if the config has been parsed successfully, 0 if any | |
2e9b2421 | 121 | * error has occurred (such as anybody calling cf_error()) and |
06607335 MM |
122 | * the @err_msg field has been set to the error message. |
123 | */ | |
31b3e1bb MM |
124 | int |
125 | config_parse(struct config *c) | |
126 | { | |
9b9a7143 | 127 | int done = 0; |
f30b86f9 | 128 | DBG("Parsing configuration file `%s'\n", c->file_name); |
31b3e1bb | 129 | new_config = c; |
31b3e1bb MM |
130 | cfg_mem = c->mem; |
131 | if (setjmp(conf_jmpbuf)) | |
9b9a7143 OZ |
132 | goto cleanup; |
133 | ||
48ec367a | 134 | cf_lex_init(0, c); |
7c0cc76e | 135 | sysdep_preconfig(c); |
31b3e1bb | 136 | protos_preconfig(c); |
0e02abfd | 137 | rt_preconfig(c); |
af582c48 | 138 | roa_preconfig(c); |
31b3e1bb | 139 | cf_parse(); |
31b3e1bb | 140 | protos_postconfig(c); |
97e46d28 OZ |
141 | if (EMPTY_LIST(c->protos)) |
142 | cf_error("No protocol is specified in the config file"); | |
dce26783 MM |
143 | #ifdef IPV6 |
144 | if (!c->router_id) | |
145 | cf_error("Router ID must be configured manually on IPv6 routers"); | |
146 | #endif | |
9b9a7143 OZ |
147 | done = 1; |
148 | ||
149 | cleanup: | |
150 | new_config = NULL; | |
151 | cfg_mem = NULL; | |
152 | return done; | |
31b3e1bb MM |
153 | } |
154 | ||
06607335 MM |
155 | /** |
156 | * cli_parse - parse a CLI command | |
157 | * @c: temporary config structure | |
158 | * | |
159 | * cli_parse() is similar to config_parse(), but instead of a configuration, | |
160 | * it parses a CLI command. See the CLI module for more information. | |
161 | */ | |
bc2fb680 MM |
162 | int |
163 | cli_parse(struct config *c) | |
164 | { | |
9b9a7143 | 165 | int done = 0; |
c9aae7f4 | 166 | c->sym_fallback = config->sym_hash; |
9b9a7143 | 167 | new_config = c; |
bc2fb680 MM |
168 | cfg_mem = c->mem; |
169 | if (setjmp(conf_jmpbuf)) | |
9b9a7143 OZ |
170 | goto cleanup; |
171 | ||
48ec367a | 172 | cf_lex_init(1, c); |
bc2fb680 | 173 | cf_parse(); |
9b9a7143 OZ |
174 | done = 1; |
175 | ||
176 | cleanup: | |
177 | c->sym_fallback = NULL; | |
178 | new_config = NULL; | |
179 | cfg_mem = NULL; | |
180 | return done; | |
bc2fb680 MM |
181 | } |
182 | ||
06607335 MM |
183 | /** |
184 | * config_free - free a configuration | |
185 | * @c: configuration to be freed | |
186 | * | |
187 | * This function takes a &config structure and frees all resources | |
188 | * associated with it. | |
189 | */ | |
31b3e1bb MM |
190 | void |
191 | config_free(struct config *c) | |
192 | { | |
a92cf57d OZ |
193 | if (c) |
194 | rfree(c->pool); | |
31b3e1bb MM |
195 | } |
196 | ||
197 | void | |
50fe90ed MM |
198 | config_add_obstacle(struct config *c) |
199 | { | |
200 | DBG("+++ adding obstacle %d\n", c->obstacle_count); | |
201 | c->obstacle_count++; | |
202 | } | |
203 | ||
204 | void | |
205 | config_del_obstacle(struct config *c) | |
31b3e1bb | 206 | { |
50fe90ed MM |
207 | DBG("+++ deleting obstacle %d\n", c->obstacle_count); |
208 | c->obstacle_count--; | |
209 | if (!c->obstacle_count) | |
a92cf57d | 210 | ev_schedule(config_event); |
50fe90ed MM |
211 | } |
212 | ||
213 | static int | |
bf8558bc | 214 | global_commit(struct config *new, struct config *old) |
50fe90ed MM |
215 | { |
216 | if (!old) | |
217 | return 0; | |
789772ed | 218 | |
d72cdff4 OZ |
219 | if (!ipa_equal(old->listen_bgp_addr, new->listen_bgp_addr) || |
220 | (old->listen_bgp_port != new->listen_bgp_port) || | |
221 | (old->listen_bgp_flags != new->listen_bgp_flags)) | |
789772ed OZ |
222 | log(L_WARN "Reconfiguration of BGP listening socket not implemented, please restart BIRD."); |
223 | ||
bf8558bc | 224 | if (!new->router_id) |
79b4e12e OZ |
225 | { |
226 | new->router_id = old->router_id; | |
227 | ||
228 | if (new->router_id_from) | |
229 | { | |
230 | u32 id = if_choose_router_id(new->router_id_from, old->router_id); | |
231 | if (!id) | |
232 | log(L_WARN "Cannot determine router ID, using old one"); | |
233 | else | |
234 | new->router_id = id; | |
235 | } | |
236 | } | |
237 | ||
50fe90ed MM |
238 | return 0; |
239 | } | |
240 | ||
241 | static int | |
bf1aec97 | 242 | config_do_commit(struct config *c, int type) |
50fe90ed | 243 | { |
a92cf57d OZ |
244 | if (type == RECONFIG_UNDO) |
245 | { | |
246 | c = old_config; | |
247 | type = old_cftype; | |
248 | } | |
249 | else | |
250 | config_free(old_config); | |
50fe90ed | 251 | |
50fe90ed | 252 | old_config = config; |
a92cf57d OZ |
253 | old_cftype = type; |
254 | config = c; | |
255 | ||
256 | configuring = 1; | |
257 | if (old_config && !config->shutdown) | |
258 | log(L_INFO "Reconfiguring"); | |
259 | ||
50fe90ed MM |
260 | if (old_config) |
261 | old_config->obstacle_count++; | |
76b53a4e | 262 | |
50fe90ed | 263 | DBG("sysdep_commit\n"); |
a92cf57d | 264 | int force_restart = sysdep_commit(c, old_config); |
50fe90ed MM |
265 | DBG("global_commit\n"); |
266 | force_restart |= global_commit(c, old_config); | |
267 | DBG("rt_commit\n"); | |
268 | rt_commit(c, old_config); | |
af582c48 | 269 | roa_commit(c, old_config); |
50fe90ed | 270 | DBG("protos_commit\n"); |
bf1aec97 | 271 | protos_commit(c, old_config, force_restart, type); |
a92cf57d | 272 | |
a92cf57d | 273 | int obs = 0; |
50fe90ed | 274 | if (old_config) |
a92cf57d OZ |
275 | obs = --old_config->obstacle_count; |
276 | ||
277 | DBG("do_commit finished with %d obstacles remaining\n", obs); | |
278 | return !obs; | |
50fe90ed MM |
279 | } |
280 | ||
8f6accb5 | 281 | static void |
7c103b1e | 282 | config_done(void *unused UNUSED) |
50fe90ed | 283 | { |
a92cf57d OZ |
284 | if (config->shutdown) |
285 | sysdep_shutdown_done(); | |
286 | ||
287 | configuring = 0; | |
288 | if (old_config) | |
289 | log(L_INFO "Reconfigured"); | |
50fe90ed | 290 | |
a92cf57d | 291 | if (future_cftype) |
50fe90ed | 292 | { |
a92cf57d OZ |
293 | int type = future_cftype; |
294 | struct config *conf = future_config; | |
295 | future_cftype = RECONFIG_NONE; | |
50fe90ed | 296 | future_config = NULL; |
a92cf57d | 297 | |
76b53a4e | 298 | log(L_INFO "Reconfiguring to queued configuration"); |
a92cf57d OZ |
299 | if (config_do_commit(conf, type)) |
300 | config_done(NULL); | |
50fe90ed | 301 | } |
50fe90ed MM |
302 | } |
303 | ||
06607335 MM |
304 | /** |
305 | * config_commit - commit a configuration | |
306 | * @c: new configuration | |
bf1aec97 | 307 | * @type: type of reconfiguration (RECONFIG_SOFT or RECONFIG_HARD) |
a92cf57d | 308 | * @timeout: timeout for undo (or 0 for no timeout) |
06607335 MM |
309 | * |
310 | * When a configuration is parsed and prepared for use, the | |
311 | * config_commit() function starts the process of reconfiguration. | |
312 | * It checks whether there is already a reconfiguration in progress | |
313 | * in which case it just queues the new config for later processing. | |
314 | * Else it notifies all modules about the new configuration by calling | |
315 | * their commit() functions which can either accept it immediately | |
316 | * or call config_add_obstacle() to report that they need some time | |
317 | * to complete the reconfiguration. After all such obstacles are removed | |
318 | * using config_del_obstacle(), the old configuration is freed and | |
319 | * everything runs according to the new one. | |
320 | * | |
a92cf57d OZ |
321 | * When @timeout is nonzero, the undo timer is activated with given |
322 | * timeout. The timer is deactivated when config_commit(), | |
323 | * config_confirm() or config_undo() is called. | |
324 | * | |
06607335 MM |
325 | * Result: %CONF_DONE if the configuration has been accepted immediately, |
326 | * %CONF_PROGRESS if it will take some time to switch to it, %CONF_QUEUED | |
327 | * if it's been queued due to another reconfiguration being in progress now | |
328 | * or %CONF_SHUTDOWN if BIRD is in shutdown mode and no new configurations | |
329 | * are accepted. | |
330 | */ | |
50fe90ed | 331 | int |
a92cf57d | 332 | config_commit(struct config *c, int type, int timeout) |
50fe90ed | 333 | { |
a92cf57d | 334 | if (shutting_down) |
50fe90ed | 335 | { |
a92cf57d OZ |
336 | config_free(c); |
337 | return CONF_SHUTDOWN; | |
50fe90ed | 338 | } |
a92cf57d OZ |
339 | |
340 | undo_available = 1; | |
341 | if (timeout > 0) | |
342 | tm_start(config_timer, timeout); | |
343 | else | |
344 | tm_stop(config_timer); | |
345 | ||
346 | if (configuring) | |
50fe90ed | 347 | { |
a92cf57d | 348 | if (future_cftype) |
50fe90ed MM |
349 | { |
350 | log(L_INFO "Queueing new configuration, ignoring the one already queued"); | |
351 | config_free(future_config); | |
352 | } | |
353 | else | |
a92cf57d OZ |
354 | log(L_INFO "Queueing new configuration"); |
355 | ||
356 | future_cftype = type; | |
50fe90ed MM |
357 | future_config = c; |
358 | return CONF_QUEUED; | |
359 | } | |
76b53a4e | 360 | |
bf1aec97 | 361 | if (config_do_commit(c, type)) |
50fe90ed MM |
362 | { |
363 | config_done(NULL); | |
364 | return CONF_DONE; | |
365 | } | |
a92cf57d OZ |
366 | return CONF_PROGRESS; |
367 | } | |
368 | ||
369 | /** | |
370 | * config_confirm - confirm a commited configuration | |
371 | * | |
372 | * When the undo timer is activated by config_commit() with nonzero timeout, | |
373 | * this function can be used to deactivate it and therefore confirm | |
374 | * the current configuration. | |
375 | * | |
376 | * Result: %CONF_CONFIRM when the current configuration is confirmed, | |
377 | * %CONF_NONE when there is nothing to confirm (i.e. undo timer is not active). | |
378 | */ | |
379 | int | |
380 | config_confirm(void) | |
381 | { | |
382 | if (config_timer->expires == 0) | |
383 | return CONF_NOTHING; | |
384 | ||
385 | tm_stop(config_timer); | |
386 | ||
387 | return CONF_CONFIRM; | |
388 | } | |
389 | ||
390 | /** | |
391 | * config_undo - undo a configuration | |
392 | * | |
393 | * Function config_undo() can be used to change the current | |
394 | * configuration back to stored %old_config. If no reconfiguration is | |
395 | * running, this stored configuration is commited in the same way as a | |
396 | * new configuration in config_commit(). If there is already a | |
397 | * reconfiguration in progress and no next reconfiguration is | |
398 | * scheduled, then the undo is scheduled for later processing as | |
399 | * usual, but if another reconfiguration is already scheduled, then | |
400 | * such reconfiguration is removed instead (i.e. undo is applied on | |
401 | * the last commit that scheduled it). | |
402 | * | |
403 | * Result: %CONF_DONE if the configuration has been accepted immediately, | |
404 | * %CONF_PROGRESS if it will take some time to switch to it, %CONF_QUEUED | |
405 | * if it's been queued due to another reconfiguration being in progress now, | |
406 | * %CONF_UNQUEUED if a scheduled reconfiguration is removed, %CONF_NOTHING | |
407 | * if there is no relevant configuration to undo (the previous config request | |
408 | * was config_undo() too) or %CONF_SHUTDOWN if BIRD is in shutdown mode and | |
409 | * no new configuration changes are accepted. | |
410 | */ | |
411 | int | |
412 | config_undo(void) | |
413 | { | |
414 | if (shutting_down) | |
415 | return CONF_SHUTDOWN; | |
416 | ||
417 | if (!undo_available || !old_config) | |
418 | return CONF_NOTHING; | |
419 | ||
420 | undo_available = 0; | |
421 | tm_stop(config_timer); | |
422 | ||
423 | if (configuring) | |
50fe90ed | 424 | { |
a92cf57d OZ |
425 | if (future_cftype) |
426 | { | |
427 | config_free(future_config); | |
428 | future_config = NULL; | |
429 | ||
430 | log(L_INFO "Removing queued configuration"); | |
431 | future_cftype = RECONFIG_NONE; | |
432 | return CONF_UNQUEUED; | |
433 | } | |
434 | else | |
435 | { | |
436 | log(L_INFO "Queueing undo configuration"); | |
437 | future_cftype = RECONFIG_UNDO; | |
438 | return CONF_QUEUED; | |
439 | } | |
440 | } | |
441 | ||
442 | if (config_do_commit(NULL, RECONFIG_UNDO)) | |
443 | { | |
444 | config_done(NULL); | |
445 | return CONF_DONE; | |
50fe90ed MM |
446 | } |
447 | return CONF_PROGRESS; | |
31b3e1bb MM |
448 | } |
449 | ||
a92cf57d OZ |
450 | extern void cmd_reconfig_undo_notify(void); |
451 | ||
452 | static void | |
453 | config_timeout(struct timer *t) | |
454 | { | |
455 | log(L_INFO "Config timeout expired, starting undo"); | |
456 | cmd_reconfig_undo_notify(); | |
457 | ||
458 | int r = config_undo(); | |
459 | if (r < 0) | |
460 | log(L_ERR "Undo request failed"); | |
461 | } | |
462 | ||
463 | void | |
464 | config_init(void) | |
465 | { | |
466 | config_event = ev_new(&root_pool); | |
467 | config_event->hook = config_done; | |
468 | ||
469 | config_timer = tm_new(&root_pool); | |
470 | config_timer->hook = config_timeout; | |
471 | } | |
472 | ||
06607335 MM |
473 | /** |
474 | * order_shutdown - order BIRD shutdown | |
475 | * | |
476 | * This function initiates shutdown of BIRD. It's accomplished by asking | |
477 | * for switching to an empty configuration. | |
478 | */ | |
bf8558bc MM |
479 | void |
480 | order_shutdown(void) | |
481 | { | |
482 | struct config *c; | |
483 | ||
484 | if (shutting_down) | |
485 | return; | |
a92cf57d | 486 | |
bf8558bc MM |
487 | log(L_INFO "Shutting down"); |
488 | c = lp_alloc(config->mem, sizeof(struct config)); | |
489 | memcpy(c, config, sizeof(struct config)); | |
490 | init_list(&c->protos); | |
491 | init_list(&c->tables); | |
492 | c->shutdown = 1; | |
a92cf57d OZ |
493 | |
494 | config_commit(c, RECONFIG_HARD, 0); | |
bf8558bc MM |
495 | shutting_down = 1; |
496 | } | |
497 | ||
06607335 MM |
498 | /** |
499 | * cf_error - report a configuration error | |
500 | * @msg: printf-like format string | |
501 | * | |
502 | * cf_error() can be called during execution of config_parse(), that is | |
503 | * from the parser, a preconfig hook or a postconfig hook, to report an | |
504 | * error in the configuration. | |
505 | */ | |
31b3e1bb MM |
506 | void |
507 | cf_error(char *msg, ...) | |
508 | { | |
509 | char buf[1024]; | |
510 | va_list args; | |
511 | ||
512 | va_start(args, msg); | |
513 | if (bvsnprintf(buf, sizeof(buf), msg, args) < 0) | |
514 | strcpy(buf, "<bug: error message too long>"); | |
515 | new_config->err_msg = cfg_strdup(buf); | |
4be266a9 OZ |
516 | new_config->err_lino = ifs->lino; |
517 | new_config->err_file_name = ifs->file_name; | |
0c3d9dac | 518 | cf_lex_unwind(); |
31b3e1bb MM |
519 | longjmp(conf_jmpbuf, 1); |
520 | } | |
521 | ||
06607335 MM |
522 | /** |
523 | * cfg_strdup - copy a string to config memory | |
524 | * @c: string to copy | |
525 | * | |
526 | * cfg_strdup() creates a new copy of the string in the memory | |
527 | * pool associated with the configuration being currently parsed. | |
528 | * It's often used when a string literal occurs in the configuration | |
529 | * and we want to preserve it for further use. | |
530 | */ | |
31b3e1bb MM |
531 | char * |
532 | cfg_strdup(char *c) | |
533 | { | |
534 | int l = strlen(c) + 1; | |
535 | char *z = cfg_allocu(l); | |
536 | memcpy(z, c, l); | |
537 | return z; | |
538 | } | |
a7f23f58 OZ |
539 | |
540 | ||
541 | void | |
542 | cfg_copy_list(list *dest, list *src, unsigned node_size) | |
543 | { | |
544 | node *dn, *sn; | |
545 | ||
546 | init_list(dest); | |
547 | WALK_LIST(sn, *src) | |
548 | { | |
549 | dn = cfg_alloc(node_size); | |
550 | memcpy(dn, sn, node_size); | |
551 | add_tail(dest, dn); | |
552 | } | |
553 | } |