• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/odhcp6c/src/config.c

  1 /**
  2  * SPDX-License-Identifier: BSD-2-Clause-Patent
  3  *
  4  * SPDX-FileCopyrightText: Copyright (c) 2024 SoftAtHome
  5  *
  6  * Redistribution and use in source and binary forms, with or
  7  * without modification, are permitted provided that the following
  8  * conditions are met:
  9  *
 10  * 1. Redistributions of source code must retain the above copyright
 11  * notice, this list of conditions and the following disclaimer.
 12  *
 13  * 2. Redistributions in binary form must reproduce the above
 14  * copyright notice, this list of conditions and the following
 15  * disclaimer in the documentation and/or other materials provided
 16  * with the distribution.
 17  *
 18  * Subject to the terms and conditions of this license, each
 19  * copyright holder and contributor hereby grants to those receiving
 20  * rights under this license a perpetual, worldwide, non-exclusive,
 21  * no-charge, royalty-free, irrevocable (except for failure to
 22  * satisfy the conditions of this license) patent license to make,
 23  * have made, use, offer to sell, sell, import, and otherwise
 24  * transfer this software, where such license applies only to those
 25  * patent claims, already acquired or hereafter acquired, licensable
 26  * by such copyright holder or contributor that are necessarily
 27  * infringed by:
 28  *
 29  * (a) their Contribution(s) (the licensed copyrights of copyright
 30  * holders and non-copyrightable additions of contributors, in
 31  * source or binary form) alone; or
 32  *
 33  * (b) combination of their Contribution(s) with the work of
 34  * authorship to which such Contribution(s) was added by such
 35  * copyright holder or contributor, if, at the time the Contribution
 36  * is added, such addition causes such combination to be necessarily
 37  * infringed. The patent license shall not apply to any other
 38  * combinations which include the Contribution.
 39  *
 40  * Except as expressly stated above, no rights or licenses from any
 41  * copyright holder or contributor is granted under this license,
 42  * whether expressly, by implication, estoppel or otherwise.
 43  *
 44  * DISCLAIMER
 45  *
 46  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 47  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 48  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 49  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 50  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
 51  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 52  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 53  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 54  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 55  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 56  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 57  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 58  * POSSIBILITY OF SUCH DAMAGE.
 59  *
 60  */
 61 
 62 #include <arpa/inet.h>
 63 #include <limits.h>
 64 #include <resolv.h>
 65 #include <stdlib.h>
 66 #include <string.h>
 67 
 68 #include "config.h"
 69 #include "odhcp6c.h"
 70 
 71 #define ARRAY_SEP " ,\t"
 72 
 73 static struct config_dhcp config_dhcp;
 74 
 75 struct config_dhcp *config_dhcp_get(void) {
 76         return &config_dhcp;
 77 }
 78 
 79 void config_dhcp_reset(void) {
 80         config_dhcp.log_level = LOG_WARNING;
 81         config_dhcp.release = true;
 82         config_dhcp.dscp = 0;
 83         config_dhcp.sk_prio = 0;
 84         config_dhcp.stateful_only_mode = false;
 85         config_dhcp.ia_na_mode = IA_MODE_TRY;
 86         config_dhcp.ia_pd_mode = IA_MODE_NONE;
 87         config_dhcp.client_options = DHCPV6_CLIENT_FQDN | DHCPV6_ACCEPT_RECONFIGURE;
 88         config_dhcp.allow_slaac_only = true;
 89         config_dhcp.oro_user_cnt = 0;
 90         memset(config_dhcp.message_rtx, 0, sizeof(config_dhcp.message_rtx));
 91         config_dhcp.message_rtx[CONFIG_DHCP_SOLICIT].delay_max = DHCPV6_MAX_DELAY;
 92         config_dhcp.message_rtx[CONFIG_DHCP_SOLICIT].timeout_init = DHCPV6_SOL_INIT_RT;
 93         config_dhcp.message_rtx[CONFIG_DHCP_SOLICIT].timeout_max = DHCPV6_SOL_MAX_RT;
 94         config_dhcp.message_rtx[CONFIG_DHCP_REQUEST].timeout_init = DHCPV6_REQ_INIT_RT;
 95         config_dhcp.message_rtx[CONFIG_DHCP_REQUEST].timeout_max = DHCPV6_REQ_MAX_RT;
 96         config_dhcp.message_rtx[CONFIG_DHCP_REQUEST].rc_max = DHCPV6_REQ_MAX_RC;
 97         config_dhcp.message_rtx[CONFIG_DHCP_RENEW].timeout_init = DHCPV6_REN_INIT_RT;
 98         config_dhcp.message_rtx[CONFIG_DHCP_RENEW].timeout_max = DHCPV6_REN_MAX_RT;
 99         config_dhcp.message_rtx[CONFIG_DHCP_REBIND].timeout_init = DHCPV6_REB_INIT_RT;
100         config_dhcp.message_rtx[CONFIG_DHCP_REBIND].timeout_max = DHCPV6_REB_MAX_RT;
101         config_dhcp.message_rtx[CONFIG_DHCP_INFO_REQ].delay_max = DHCPV6_MAX_DELAY;
102         config_dhcp.message_rtx[CONFIG_DHCP_INFO_REQ].timeout_init = DHCPV6_INF_INIT_RT;
103         config_dhcp.message_rtx[CONFIG_DHCP_INFO_REQ].timeout_max = DHCPV6_INF_MAX_RT;
104         config_dhcp.message_rtx[CONFIG_DHCP_RELEASE].timeout_init = DHCPV6_REL_INIT_RT;
105         config_dhcp.message_rtx[CONFIG_DHCP_RELEASE].rc_max = DHCPV6_REL_MAX_RC;
106         config_dhcp.message_rtx[CONFIG_DHCP_DECLINE].timeout_init = DHCPV6_DEC_INIT_RT;
107         config_dhcp.message_rtx[CONFIG_DHCP_DECLINE].rc_max = DHCPV6_DEC_MAX_RC;
108         config_dhcp.irt_default = DHCPV6_IRT_DEFAULT;
109         config_dhcp.irt_min = DHCPV6_IRT_MIN;
110         config_dhcp.rand_factor = DHCPV6_RAND_FACTOR;
111         config_dhcp.auth_protocol = AUTH_PROT_RKAP;
112         free(config_dhcp.auth_token);
113         config_dhcp.auth_token = NULL;
114 }
115 
116 void config_set_release(bool enable) {
117         config_dhcp.release = enable;
118 }
119 
120 bool config_set_dscp(unsigned int value) {
121         if (value > 63) {
122                 error("Invalid DSCP value");
123                 return false;
124         }
125         config_dhcp.dscp = value;
126         return true;
127 }
128 
129 bool config_set_sk_priority(unsigned int priority) {
130         if (priority > 6) {
131                 error("Invalid SK priority value");
132                 return false;
133         }
134         config_dhcp.sk_prio = priority;
135         return true;
136 }
137 
138 void config_set_client_options(enum dhcpv6_config option, bool enable) {
139         if (enable) {
140                 config_dhcp.client_options |= option;
141         } else {
142                 config_dhcp.client_options &= ~option;
143         }
144 }
145 
146 bool config_set_request_addresses(char* mode) {
147         if (!strcmp(mode, "force")) {
148                 config_dhcp.ia_na_mode = IA_MODE_FORCE;
149                 config_dhcp.allow_slaac_only = false;
150         } else if (!strcmp(mode, "none")) {
151                 config_dhcp.ia_na_mode = IA_MODE_NONE;
152         } else if (!strcmp(mode, "try")) {
153                 config_dhcp.ia_na_mode = IA_MODE_TRY;
154         } else {
155                 error("Invalid IA_NA Request Addresses mode");
156                 return false;
157         }
158 
159         return true;
160 }
161 
162 bool config_set_request_prefix(unsigned int length, unsigned int id) {
163         struct odhcp6c_request_prefix prefix = {0};
164 
165         odhcp6c_clear_state(STATE_IA_PD_INIT);
166 
167         if (config_dhcp.ia_pd_mode != IA_MODE_FORCE)
168                 config_dhcp.ia_pd_mode = length > 128 ? IA_MODE_NONE : IA_MODE_TRY;
169 
170         if (length <= 128) {
171                 prefix.length = length;
172                 prefix.iaid = htonl(id);
173 
174                 if (odhcp6c_add_state(STATE_IA_PD_INIT, &prefix, sizeof(prefix))) {
175                         error("Failed to set request IPv6-Prefix");
176                         return false;
177                 }
178         }
179 
180         return true;
181 }
182 
183 void config_set_force_prefix(bool enable) {
184         if (enable) {
185                 config_dhcp.allow_slaac_only = false;
186                 config_dhcp.ia_pd_mode = IA_MODE_FORCE;
187         } else {
188                 config_dhcp.ia_pd_mode = IA_MODE_NONE;
189         }
190 }
191 
192 void config_set_stateful_only(bool enable) {
193         config_dhcp.stateful_only_mode = enable;
194 }
195 
196 void config_set_allow_slaac_only(bool value) {
197         config_dhcp.allow_slaac_only = value;
198 }
199 
200 void config_clear_requested_options(void) {
201         config_dhcp.oro_user_cnt = 0;
202 }
203 
204 bool config_add_requested_options(unsigned int option) {
205         if (option > UINT16_MAX) {
206                 error("Invalid requested option");
207                 return false;
208         }
209 
210         option = htons(option);
211         if (odhcp6c_insert_state(STATE_ORO, 0, &option, 2)) {
212                 error("Failed to set requested option");
213                 return false;
214         }
215         config_dhcp.oro_user_cnt++;
216         return true;
217 }
218 
219 void config_clear_send_options(void) {
220         odhcp6c_clear_state(STATE_OPTS);
221 }
222 
223 bool config_add_send_options(char* option) {
224         return (config_parse_opt(option) == 0);
225 }
226 
227 bool config_set_rtx_delay_max(enum config_dhcp_msg msg, unsigned int value)
228 {
229         if (msg >= CONFIG_DHCP_MAX || value > UINT8_MAX) {
230                 error("Invalid retransmission Maximum Delay value");
231                 return false;
232         }
233         config_dhcp.message_rtx[msg].delay_max = value;
234         return true;
235 }
236 
237 bool config_set_rtx_timeout_init(enum config_dhcp_msg msg, unsigned int value)
238 {
239         if (msg >= CONFIG_DHCP_MAX || value > UINT8_MAX || value == 0) {
240                 error("Invalid retransmission Initial Timeout value");
241                 return false;
242         }
243         config_dhcp.message_rtx[msg].timeout_init = value;
244         return true;
245 }
246 
247 bool config_set_rtx_timeout_max(enum config_dhcp_msg msg, unsigned int value)
248 {
249         if (msg >= CONFIG_DHCP_MAX || value > UINT16_MAX) {
250                 error("Invalid retransmission Maximum Timeout value");
251                 return false;
252         }
253         config_dhcp.message_rtx[msg].timeout_max = value;
254         return true;
255 }
256 
257 bool config_set_rtx_rc_max(enum config_dhcp_msg msg, unsigned int value)
258 {
259         if (msg >= CONFIG_DHCP_MAX || value > UINT8_MAX) {
260                 error("Invalid retransmission Retry Attempt value");
261                 return false;
262         }
263         config_dhcp.message_rtx[msg].rc_max = value;
264         return true;
265 }
266 
267 bool config_set_irt_default(unsigned int value)
268 {
269         if (value == 0) {
270                 error("Invalid Default Information Refresh Time value");
271                 return false;
272         }
273         config_dhcp.irt_default = value;
274         return true;
275 }
276 
277 bool config_set_irt_min(unsigned int value)
278 {
279         if (value == 0) {
280                 error("Invalid Minimum Information Refresh Time value");
281                 return false;
282         }
283         config_dhcp.irt_min = value;
284         return true;
285 }
286 
287 bool config_set_rand_factor(unsigned int value)
288 {
289         if (value > 999 || value < 10) {
290                 error("Invalid Random Factor value");
291                 return false;
292         }
293         config_dhcp.rand_factor = value;
294         return true;
295 }
296 
297 bool config_set_auth_protocol(const char* protocol)
298 {
299         if (!strcmp(protocol, "None")) {
300                 config_dhcp.auth_protocol = AUTH_PROT_NONE;
301         } else if (!strcmp(protocol, "ConfigurationToken")) {
302                 config_dhcp.auth_protocol = AUTH_PROT_TOKEN;
303         } else if (!strcmp(protocol, "ReconfigureKeyAuthentication")) {
304                 config_dhcp.auth_protocol = AUTH_PROT_RKAP;
305         } else {
306                 error("Invalid Authentication protocol");
307                 return false;
308         }
309 
310         return true;
311 }
312 
313 bool config_set_auth_token(const char* token)
314 {
315         free(config_dhcp.auth_token);
316 
317         config_dhcp.auth_token = strdup(token);
318         return config_dhcp.auth_token != NULL;
319 }
320 
321 static int config_parse_opt_u8(const char *src, uint8_t **dst)
322 {
323         int len = strlen(src);
324 
325         uint8_t *tmp = realloc(*dst, len/2);
326         if (!tmp)
327                 return -1;
328         *dst = tmp;
329 
330         return script_unhexlify(*dst, len, src);
331 }
332 
333 static int config_parse_opt_string(const char *src, uint8_t **dst, const bool array)
334 {
335         int o_len = 0;
336         char *sep = strpbrk(src, ARRAY_SEP);
337 
338         if (sep && !array)
339                 return -1;
340 
341         do {
342                 if (sep) {
343                         *sep = 0;
344                         sep++;
345                 }
346 
347                 int len = strlen(src);
348 
349                 uint8_t *tmp = realloc(*dst, o_len + len);
350                 if (!tmp)
351                         return -1;
352                 *dst = tmp;
353 
354                 memcpy(&((*dst)[o_len]), src, len);
355 
356                 o_len += len;
357                 src = sep;
358 
359                 if (sep)
360                         sep = strpbrk(src, ARRAY_SEP);
361         } while (src);
362 
363         return o_len;
364 }
365 
366 static int config_parse_opt_dns_string(const char *src, uint8_t **dst, const bool array)
367 {
368         int o_len = 0;
369         char *sep = strpbrk(src, ARRAY_SEP);
370 
371         if (sep && !array)
372                 return -1;
373 
374         do {
375                 uint8_t tmp[256];
376 
377                 if (sep) {
378                         *sep = 0;
379                         sep++;
380                 }
381 
382                 int len = dn_comp(src, tmp, sizeof(tmp), NULL, NULL);
383                 if (len < 0)
384                         return -1;
385 
386                 uint8_t *dst_tmp = realloc(*dst, o_len + len);
387                 if (!dst_tmp)
388                         return -1;
389                 *dst = dst_tmp;
390 
391                 memcpy(&((*dst)[o_len]), tmp, len);
392 
393                 o_len += len;
394                 src = sep;
395 
396                 if (sep)
397                         sep = strpbrk(src, ARRAY_SEP);
398         } while (src);
399 
400         return o_len;
401 }
402 
403 static int config_parse_opt_ip6(const char *src, uint8_t **dst, const bool array)
404 {
405         int o_len = 0;
406         char *sep = strpbrk(src, ARRAY_SEP);
407 
408         if (sep && !array)
409                 return -1;
410 
411         do {
412                 int len = sizeof(struct in6_addr);
413 
414                 if (sep) {
415                         *sep = 0;
416                         sep++;
417                 }
418 
419                 uint8_t *tmp = realloc(*dst, o_len + len);
420                 if (!tmp)
421                         return -1;
422                 *dst = tmp;
423 
424                 if (inet_pton(AF_INET6, src, &((*dst)[o_len])) < 1)
425                         return -1;
426 
427                 o_len += len;
428                 src = sep;
429 
430                 if (sep)
431                         sep = strpbrk(src, ARRAY_SEP);
432         } while (src);
433 
434         return o_len;
435 }
436 
437 static int config_parse_opt_user_class(const char *src, uint8_t **dst, const bool array)
438 {
439         int o_len = 0;
440         char *sep = strpbrk(src, ARRAY_SEP);
441 
442         if (sep && !array)
443                 return -1;
444 
445         do {
446                 if (sep) {
447                         *sep = 0;
448                         sep++;
449                 }
450                 uint16_t str_len = strlen(src);
451 
452                 uint8_t *tmp = realloc(*dst, o_len + str_len + 2);
453                 if (!tmp)
454                         return -1;
455                 *dst = tmp;
456 
457                 struct user_class {
458                         uint16_t len;
459                         uint8_t data[];
460                 } *e = (struct user_class *)&((*dst)[o_len]);
461 
462                 e->len = ntohs(str_len);
463                 memcpy(e->data, src, str_len);
464 
465                 o_len += str_len + 2;
466                 src = sep;
467 
468                 if (sep)
469                         sep = strpbrk(src, ARRAY_SEP);
470         } while (src);
471 
472         return o_len;
473 }
474 
475 static uint8_t *config_state_find_opt(const uint16_t code)
476 {
477         size_t opts_len;
478         uint8_t *odata, *opts = odhcp6c_get_state(STATE_OPTS, &opts_len);
479         uint16_t otype, olen;
480 
481         dhcpv6_for_each_option(opts, &opts[opts_len], otype, olen, odata) {
482                 if (otype == code)
483                         return &odata[-4];
484         }
485 
486         return NULL;
487 }
488 
489 int config_add_opt(const uint16_t code, const uint8_t *data, const uint16_t len)
490 {
491         struct {
492                 uint16_t code;
493                 uint16_t len;
494         } opt_hdr = { htons(code), htons(len) };
495 
496         if (config_state_find_opt(code))
497                 return -1;
498 
499         if (odhcp6c_add_state(STATE_OPTS, &opt_hdr, sizeof(opt_hdr)) ||
500                         odhcp6c_add_state(STATE_OPTS, data, len)) {
501                 error("Failed to add option %hu", code);
502                 return 1;
503         }
504 
505         return 0;
506 }
507 
508 int config_parse_opt_data(const char *data, uint8_t **dst, const unsigned int type,
509                 const bool array)
510 {
511         int ret = 0;
512 
513         switch (type) {
514         case OPT_U8:
515                 ret = config_parse_opt_u8(data, dst);
516                 break;
517 
518         case OPT_STR:
519                 ret = config_parse_opt_string(data, dst, array);
520                 break;
521 
522         case OPT_DNS_STR:
523                 ret = config_parse_opt_dns_string(data, dst, array);
524                 break;
525 
526         case OPT_IP6:
527                 ret = config_parse_opt_ip6(data, dst, array);
528                 break;
529 
530         case OPT_USER_CLASS:
531                 ret = config_parse_opt_user_class(data, dst, array);
532                 break;
533 
534         default:
535                 ret = -1;
536                 break;
537         }
538 
539         return ret;
540 }
541 
542 int config_parse_opt(const char *opt)
543 {
544         uint32_t optn;
545         char *data;
546         uint8_t *payload = NULL;
547         int payload_len;
548         unsigned int type = OPT_U8;
549         bool array = false;
550         struct odhcp6c_opt *dopt = NULL;
551         int ret = -1;
552 
553         data = strpbrk(opt, ":");
554         if (!data)
555                 return -1;
556 
557         *data = '\0';
558         data++;
559 
560         if (strlen(opt) == 0 || strlen(data) == 0)
561                 return -1;
562 
563         dopt = odhcp6c_find_opt_by_name(opt);
564         if (!dopt) {
565                 char *e;
566                 optn = strtoul(opt, &e, 0);
567                 if (*e || e == opt || optn > USHRT_MAX)
568                         return -1;
569 
570                 dopt = odhcp6c_find_opt(optn);
571         } else {
572                 optn = dopt->code;
573         }
574 
575         /* Check if the type for the content is well-known */
576         if (dopt) {
577                 /* Refuse internal options */
578                 if (dopt->flags & OPT_INTERNAL)
579                         return -1;
580 
581                 type = dopt->flags & OPT_MASK_SIZE;
582                 array = ((dopt->flags & OPT_ARRAY) == OPT_ARRAY) ? true : false;
583         } else if (data[0] == '"' || data[0] == '\'') {
584                 char *end = strrchr(data + 1, data[0]);
585 
586                 if (end && (end == (data + strlen(data) - 1))) {
587                         /* Raw option is specified as a string */
588                         type = OPT_STR;
589                         data++;
590                         *end = '\0';
591                 }
592         }
593 
594         payload_len = config_parse_opt_data(data, &payload, type, array);
595         if (payload_len > 0)
596                 ret = config_add_opt(optn, payload, payload_len);
597 
598         free(payload);
599 
600         return ret;
601 }
602 
603 void config_apply_dhcp_rtx(struct dhcpv6_retx* dhcpv6_retx)
604 {
605         dhcpv6_retx[DHCPV6_MSG_SOLICIT].max_delay = config_dhcp.message_rtx[CONFIG_DHCP_SOLICIT].delay_max;
606         dhcpv6_retx[DHCPV6_MSG_SOLICIT].init_timeo = config_dhcp.message_rtx[CONFIG_DHCP_SOLICIT].timeout_init;
607         dhcpv6_retx[DHCPV6_MSG_SOLICIT].max_timeo = config_dhcp.message_rtx[CONFIG_DHCP_SOLICIT].timeout_max;
608         dhcpv6_retx[DHCPV6_MSG_REQUEST].init_timeo = config_dhcp.message_rtx[CONFIG_DHCP_REQUEST].timeout_init;
609         dhcpv6_retx[DHCPV6_MSG_REQUEST].max_timeo = config_dhcp.message_rtx[CONFIG_DHCP_REQUEST].timeout_max;
610         dhcpv6_retx[DHCPV6_MSG_REQUEST].max_rc = config_dhcp.message_rtx[CONFIG_DHCP_REQUEST].rc_max;
611         dhcpv6_retx[DHCPV6_MSG_RENEW].init_timeo = config_dhcp.message_rtx[CONFIG_DHCP_RENEW].timeout_init;
612         dhcpv6_retx[DHCPV6_MSG_RENEW].max_timeo = config_dhcp.message_rtx[CONFIG_DHCP_RENEW].timeout_max;
613         dhcpv6_retx[DHCPV6_MSG_REBIND].init_timeo = config_dhcp.message_rtx[CONFIG_DHCP_REBIND].timeout_init;
614         dhcpv6_retx[DHCPV6_MSG_REBIND].max_timeo = config_dhcp.message_rtx[CONFIG_DHCP_REBIND].timeout_max;
615         dhcpv6_retx[DHCPV6_MSG_INFO_REQ].max_delay = config_dhcp.message_rtx[CONFIG_DHCP_INFO_REQ].delay_max;
616         dhcpv6_retx[DHCPV6_MSG_INFO_REQ].init_timeo = config_dhcp.message_rtx[CONFIG_DHCP_INFO_REQ].timeout_init;
617         dhcpv6_retx[DHCPV6_MSG_INFO_REQ].max_timeo = config_dhcp.message_rtx[CONFIG_DHCP_INFO_REQ].timeout_max;
618         dhcpv6_retx[DHCPV6_MSG_RELEASE].init_timeo = config_dhcp.message_rtx[CONFIG_DHCP_RELEASE].timeout_init;
619         dhcpv6_retx[DHCPV6_MSG_RELEASE].max_rc = config_dhcp.message_rtx[CONFIG_DHCP_RELEASE].rc_max;
620         dhcpv6_retx[DHCPV6_MSG_DECLINE].init_timeo = config_dhcp.message_rtx[CONFIG_DHCP_DECLINE].timeout_init;
621         dhcpv6_retx[DHCPV6_MSG_DECLINE].max_rc = config_dhcp.message_rtx[CONFIG_DHCP_DECLINE].rc_max;
622 }
623 

This page was automatically generated by LXR 0.3.1.  •  OpenWrt