1/* $NetBSD: chap-new.c,v 1.5 2021/01/09 16:39:28 christos Exp $ */ 2 3/* 4 * chap-new.c - New CHAP implementation. 5 * 6 * Copyright (c) 2003 Paul Mackerras. All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 15 * 2. The name(s) of the authors of this software must not be used to 16 * endorse or promote products derived from this software without 17 * prior written permission. 18 * 19 * 3. Redistributions of any form whatsoever must retain the following 20 * acknowledgment: 21 * "This product includes software developed by Paul Mackerras 22 * <paulus@samba.org>". 23 * 24 * THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO 25 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 26 * AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY 27 * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 28 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 29 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING 30 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 31 */ 32 33#include <sys/cdefs.h> 34#if 0 35#define RCSID "Id: chap-new.c,v 1.9 2007/06/19 02:08:35 carlsonj Exp " 36static const char rcsid[] = RCSID; 37#else 38__RCSID("$NetBSD: chap-new.c,v 1.5 2021/01/09 16:39:28 christos Exp $"); 39#endif 40 41#include <stdlib.h> 42#include <string.h> 43#include "pppd.h" 44#include "session.h" 45#include "chap-new.h" 46#include "chap-md5.h" 47 48#ifdef CHAPMS 49#include "chap_ms.h" 50#define MDTYPE_ALL (MDTYPE_MICROSOFT_V2 | MDTYPE_MICROSOFT | MDTYPE_MD5) 51#else 52#define MDTYPE_ALL (MDTYPE_MD5) 53#endif 54 55int chap_mdtype_all = MDTYPE_ALL; 56 57/* Hook for a plugin to validate CHAP challenge */ 58int (*chap_verify_hook)(char *name, char *ourname, int id, 59 struct chap_digest_type *digest, 60 unsigned char *challenge, unsigned char *response, 61 char *message, int message_space) = NULL; 62 63/* 64 * Option variables. 65 */ 66int chap_server_timeout_time = 3; 67int chap_max_transmits = 10; 68int chap_rechallenge_time = 0; 69int chap_client_timeout_time = 60; 70int chapms_strip_domain = 0; 71 72/* 73 * Command-line options. 74 */ 75static option_t chap_option_list[] = { 76 { "chap-restart", o_int, &chap_server_timeout_time, 77 "Set timeout for CHAP (as server)", OPT_PRIO }, 78 { "chap-max-challenge", o_int, &chap_max_transmits, 79 "Set max #xmits for challenge", OPT_PRIO }, 80 { "chap-interval", o_int, &chap_rechallenge_time, 81 "Set interval for rechallenge", OPT_PRIO }, 82 { "chap-timeout", o_int, &chap_client_timeout_time, 83 "Set timeout for CHAP (as client)", OPT_PRIO }, 84 { "chapms-strip-domain", o_bool, &chapms_strip_domain, 85 "Strip the domain prefix before the Username", 1 }, 86 { NULL } 87}; 88 89/* 90 * Internal state. 91 */ 92static struct chap_client_state { 93 int flags; 94 char *name; 95 struct chap_digest_type *digest; 96 unsigned char priv[64]; /* private area for digest's use */ 97} client; 98 99/* 100 * These limits apply to challenge and response packets we send. 101 * The +4 is the +1 that we actually need rounded up. 102 */ 103#define CHAL_MAX_PKTLEN (PPP_HDRLEN + CHAP_HDRLEN + 4 + MAX_CHALLENGE_LEN + MAXNAMELEN) 104#define RESP_MAX_PKTLEN (PPP_HDRLEN + CHAP_HDRLEN + 4 + MAX_RESPONSE_LEN + MAXNAMELEN) 105 106static struct chap_server_state { 107 int flags; 108 int id; 109 char *name; 110 struct chap_digest_type *digest; 111 int challenge_xmits; 112 int challenge_pktlen; 113 unsigned char challenge[CHAL_MAX_PKTLEN]; 114 char message[256]; 115} server; 116 117/* Values for flags in chap_client_state and chap_server_state */ 118#define LOWERUP 1 119#define AUTH_STARTED 2 120#define AUTH_DONE 4 121#define AUTH_FAILED 8 122#define TIMEOUT_PENDING 0x10 123#define CHALLENGE_VALID 0x20 124 125/* 126 * Prototypes. 127 */ 128static void chap_init(int unit); 129static void chap_lowerup(int unit); 130static void chap_lowerdown(int unit); 131static void chap_server_timeout(void *arg); 132static void chap_client_timeout(void *arg); 133static void chap_generate_challenge(struct chap_server_state *ss); 134static void chap_handle_response(struct chap_server_state *ss, int code, 135 unsigned char *pkt, int len); 136static int chap_verify_response(char *name, char *ourname, int id, 137 struct chap_digest_type *digest, 138 unsigned char *challenge, unsigned char *response, 139 char *message, int message_space); 140static void chap_respond(struct chap_client_state *cs, int id, 141 unsigned char *pkt, int len); 142static void chap_handle_status(struct chap_client_state *cs, int code, int id, 143 unsigned char *pkt, int len); 144static void chap_protrej(int unit); 145static void chap_input(int unit, unsigned char *pkt, int pktlen); 146static int chap_print_pkt(unsigned char *p, int plen, 147 void (*printer)(void *, char *, ...), void *arg); 148 149/* List of digest types that we know about */ 150static struct chap_digest_type *chap_digests; 151 152/* 153 * chap_init - reset to initial state. 154 */ 155static void 156chap_init(int unit) 157{ 158 memset(&client, 0, sizeof(client)); 159 memset(&server, 0, sizeof(server)); 160 161 chap_md5_init(); 162#ifdef CHAPMS 163 chapms_init(); 164#endif 165} 166 167/* 168 * Add a new digest type to the list. 169 */ 170void 171chap_register_digest(struct chap_digest_type *dp) 172{ 173 dp->next = chap_digests; 174 chap_digests = dp; 175} 176 177/* 178 * Lookup a digest type by code 179 */ 180struct chap_digest_type * 181chap_find_digest(int digest_code) { 182 struct chap_digest_type *dp = NULL; 183 for (dp = chap_digests; dp != NULL; dp = dp->next) 184 if (dp->code == digest_code) 185 break; 186 return dp; 187} 188 189/* 190 * chap_lowerup - we can start doing stuff now. 191 */ 192static void 193chap_lowerup(int unit) 194{ 195 struct chap_client_state *cs = &client; 196 struct chap_server_state *ss = &server; 197 198 cs->flags |= LOWERUP; 199 ss->flags |= LOWERUP; 200 if (ss->flags & AUTH_STARTED) 201 chap_server_timeout(ss); 202} 203 204static void 205chap_lowerdown(int unit) 206{ 207 struct chap_client_state *cs = &client; 208 struct chap_server_state *ss = &server; 209 210 if (cs->flags & TIMEOUT_PENDING) 211 UNTIMEOUT(chap_client_timeout, cs); 212 cs->flags = 0; 213 if (ss->flags & TIMEOUT_PENDING) 214 UNTIMEOUT(chap_server_timeout, ss); 215 ss->flags = 0; 216} 217 218/* 219 * chap_auth_peer - Start authenticating the peer. 220 * If the lower layer is already up, we start sending challenges, 221 * otherwise we wait for the lower layer to come up. 222 */ 223void 224chap_auth_peer(int unit, char *our_name, int digest_code) 225{ 226 struct chap_server_state *ss = &server; 227 struct chap_digest_type *dp; 228 229 if (ss->flags & AUTH_STARTED) { 230 error("CHAP: peer authentication already started!"); 231 return; 232 } 233 for (dp = chap_digests; dp != NULL; dp = dp->next) 234 if (dp->code == digest_code) 235 break; 236 if (dp == NULL) 237 fatal("CHAP digest 0x%x requested but not available", 238 digest_code); 239 240 ss->digest = dp; 241 ss->name = our_name; 242 /* Start with a random ID value */ 243 ss->id = (unsigned char)(drand48() * 256); 244 ss->flags |= AUTH_STARTED; 245 if (ss->flags & LOWERUP) 246 chap_server_timeout(ss); 247} 248 249/* 250 * chap_auth_with_peer - Prepare to authenticate ourselves to the peer. 251 * There isn't much to do until we receive a challenge. 252 */ 253void 254chap_auth_with_peer(int unit, char *our_name, int digest_code) 255{ 256 struct chap_client_state *cs = &client; 257 struct chap_digest_type *dp; 258 259 if (cs->flags & AUTH_STARTED) { 260 error("CHAP: authentication with peer already started!"); 261 return; 262 } 263 for (dp = chap_digests; dp != NULL; dp = dp->next) 264 if (dp->code == digest_code) 265 break; 266 if (dp == NULL) 267 fatal("CHAP digest 0x%x requested but not available", 268 digest_code); 269 270 cs->digest = dp; 271 cs->name = our_name; 272 cs->flags |= AUTH_STARTED | TIMEOUT_PENDING; 273 TIMEOUT(chap_client_timeout, cs, chap_client_timeout_time); 274} 275 276/* 277 * chap_server_timeout - It's time to send another challenge to the peer. 278 * This could be either a retransmission of a previous challenge, 279 * or a new challenge to start re-authentication. 280 */ 281static void 282chap_server_timeout(void *arg) 283{ 284 struct chap_server_state *ss = arg; 285 286 ss->flags &= ~TIMEOUT_PENDING; 287 if ((ss->flags & CHALLENGE_VALID) == 0) { 288 ss->challenge_xmits = 0; 289 chap_generate_challenge(ss); 290 ss->flags |= CHALLENGE_VALID; 291 } else if (ss->challenge_xmits >= chap_max_transmits) { 292 ss->flags &= ~CHALLENGE_VALID; 293 ss->flags |= AUTH_DONE | AUTH_FAILED; 294 auth_peer_fail(0, PPP_CHAP); 295 return; 296 } 297 298 output(0, ss->challenge, ss->challenge_pktlen); 299 ++ss->challenge_xmits; 300 ss->flags |= TIMEOUT_PENDING; 301 TIMEOUT(chap_server_timeout, arg, chap_server_timeout_time); 302} 303 304/* chap_client_timeout - Authentication with peer timed out. */ 305static void 306chap_client_timeout(void *arg) 307{ 308 struct chap_client_state *cs = arg; 309 310 cs->flags &= ~TIMEOUT_PENDING; 311 cs->flags |= AUTH_DONE | AUTH_FAILED; 312 error("CHAP authentication timed out"); 313 auth_withpeer_fail(0, PPP_CHAP); 314} 315 316/* 317 * chap_generate_challenge - generate a challenge string and format 318 * the challenge packet in ss->challenge_pkt. 319 */ 320static void 321chap_generate_challenge(struct chap_server_state *ss) 322{ 323 int clen = 1, nlen, len; 324 unsigned char *p; 325 326 p = ss->challenge; 327 MAKEHEADER(p, PPP_CHAP); 328 p += CHAP_HDRLEN; 329 ss->digest->generate_challenge(p); 330 clen = *p; 331 nlen = strlen(ss->name); 332 memcpy(p + 1 + clen, ss->name, nlen); 333 334 len = CHAP_HDRLEN + 1 + clen + nlen; 335 ss->challenge_pktlen = PPP_HDRLEN + len; 336 337 p = ss->challenge + PPP_HDRLEN; 338 p[0] = CHAP_CHALLENGE; 339 p[1] = ++ss->id; 340 p[2] = len >> 8; 341 p[3] = len; 342} 343 344/* 345 * chap_handle_response - check the response to our challenge. 346 */ 347static void 348chap_handle_response(struct chap_server_state *ss, int id, 349 unsigned char *pkt, int len) 350{ 351 int response_len, ok, mlen; 352 unsigned char *response, *p; 353 char *name = NULL; /* initialized to shut gcc up */ 354 int (*verifier)(char *, char *, int, struct chap_digest_type *, 355 unsigned char *, unsigned char *, char *, int); 356 char rname[MAXNAMELEN+1]; 357 358 if ((ss->flags & LOWERUP) == 0) 359 return; 360 if (id != ss->challenge[PPP_HDRLEN+1] || len < 2) 361 return; 362 if (ss->flags & CHALLENGE_VALID) { 363 response = pkt; 364 GETCHAR(response_len, pkt); 365 len -= response_len + 1; /* length of name */ 366 name = (char *)pkt + response_len; 367 if (len < 0) 368 return; 369 370 if (ss->flags & TIMEOUT_PENDING) { 371 ss->flags &= ~TIMEOUT_PENDING; 372 UNTIMEOUT(chap_server_timeout, ss); 373 } 374 375 if (explicit_remote) { 376 name = remote_name; 377 } else { 378 /* Null terminate and clean remote name. */ 379 slprintf(rname, sizeof(rname), "%.*v", len, name); 380 name = rname; 381 382 /* strip the MS domain name */ 383 if (chapms_strip_domain && strrchr(rname, '\\')) { 384 char tmp[MAXNAMELEN+1]; 385 386 strcpy(tmp, strrchr(rname, '\\') + 1); 387 strcpy(rname, tmp); 388 } 389 } 390 391 if (chap_verify_hook) 392 verifier = chap_verify_hook; 393 else 394 verifier = chap_verify_response; 395 ok = (*verifier)(name, ss->name, id, ss->digest, 396 ss->challenge + PPP_HDRLEN + CHAP_HDRLEN, 397 response, ss->message, sizeof(ss->message)); 398 if (!ok || !auth_number()) { 399 ss->flags |= AUTH_FAILED; 400 warn("Peer %q failed CHAP authentication", name); 401 } 402 } else if ((ss->flags & AUTH_DONE) == 0) 403 return; 404 405 /* send the response */ 406 p = outpacket_buf; 407 MAKEHEADER(p, PPP_CHAP); 408 mlen = strlen(ss->message); 409 len = CHAP_HDRLEN + mlen; 410 p[0] = (ss->flags & AUTH_FAILED)? CHAP_FAILURE: CHAP_SUCCESS; 411 p[1] = id; 412 p[2] = len >> 8; 413 p[3] = len; 414 if (mlen > 0) 415 memcpy(p + CHAP_HDRLEN, ss->message, mlen); 416 output(0, outpacket_buf, PPP_HDRLEN + len); 417 418 if (ss->flags & CHALLENGE_VALID) { 419 ss->flags &= ~CHALLENGE_VALID; 420 if (!(ss->flags & AUTH_DONE) && !(ss->flags & AUTH_FAILED)) { 421 /* 422 * Auth is OK, so now we need to check session restrictions 423 * to ensure everything is OK, but only if we used a 424 * plugin, and only if we're configured to check. This 425 * allows us to do PAM checks on PPP servers that 426 * authenticate against ActiveDirectory, and use AD for 427 * account info (like when using Winbind integrated with 428 * PAM). 429 */ 430 if (session_mgmt && 431 session_check(name, NULL, devnam, NULL) == 0) { 432 ss->flags |= AUTH_FAILED; 433 warn("Peer %q failed CHAP Session verification", name); 434 } 435 } 436 if (ss->flags & AUTH_FAILED) { 437 auth_peer_fail(0, PPP_CHAP); 438 } else { 439 if ((ss->flags & AUTH_DONE) == 0) 440 auth_peer_success(0, PPP_CHAP, 441 ss->digest->code, 442 name, strlen(name)); 443 if (chap_rechallenge_time) { 444 ss->flags |= TIMEOUT_PENDING; 445 TIMEOUT(chap_server_timeout, ss, 446 chap_rechallenge_time); 447 } 448 } 449 ss->flags |= AUTH_DONE; 450 } 451} 452 453/* 454 * chap_verify_response - check whether the peer's response matches 455 * what we think it should be. Returns 1 if it does (authentication 456 * succeeded), or 0 if it doesn't. 457 */ 458static int 459chap_verify_response(char *name, char *ourname, int id, 460 struct chap_digest_type *digest, 461 unsigned char *challenge, unsigned char *response, 462 char *message, int message_space) 463{ 464 int ok; 465 unsigned char secret[MAXSECRETLEN]; 466 int secret_len; 467 468 /* Get the secret that the peer is supposed to know */ 469 if (!get_secret(0, name, ourname, (char *)secret, &secret_len, 1)) { 470 error("No CHAP secret found for authenticating %q", name); 471 return 0; 472 } 473 474 ok = digest->verify_response(id, name, secret, secret_len, challenge, 475 response, message, message_space); 476 memset(secret, 0, sizeof(secret)); 477 478 return ok; 479} 480 481/* 482 * chap_respond - Generate and send a response to a challenge. 483 */ 484static void 485chap_respond(struct chap_client_state *cs, int id, 486 unsigned char *pkt, int len) 487{ 488 int clen, nlen; 489 int secret_len; 490 unsigned char *p; 491 unsigned char response[RESP_MAX_PKTLEN]; 492 char rname[MAXNAMELEN+1]; 493 char secret[MAXSECRETLEN+1]; 494 495 if ((cs->flags & (LOWERUP | AUTH_STARTED)) != (LOWERUP | AUTH_STARTED)) 496 return; /* not ready */ 497 if (len < 2 || len < pkt[0] + 1) 498 return; /* too short */ 499 clen = pkt[0]; 500 nlen = len - (clen + 1); 501 502 /* Null terminate and clean remote name. */ 503 slprintf(rname, sizeof(rname), "%.*v", nlen, pkt + clen + 1); 504 505 /* Microsoft doesn't send their name back in the PPP packet */ 506 if (explicit_remote || (remote_name[0] != 0 && rname[0] == 0)) 507 strlcpy(rname, remote_name, sizeof(rname)); 508 509 /* get secret for authenticating ourselves with the specified host */ 510 if (!get_secret(0, cs->name, rname, secret, &secret_len, 0)) { 511 secret_len = 0; /* assume null secret if can't find one */ 512 warn("No CHAP secret found for authenticating us to %q", rname); 513 } 514 515 p = response; 516 MAKEHEADER(p, PPP_CHAP); 517 p += CHAP_HDRLEN; 518 519 cs->digest->make_response(p, id, cs->name, pkt, 520 secret, secret_len, cs->priv); 521 memset(secret, 0, secret_len); 522 523 clen = *p; 524 nlen = strlen(cs->name); 525 memcpy(p + clen + 1, cs->name, nlen); 526 527 p = response + PPP_HDRLEN; 528 len = CHAP_HDRLEN + clen + 1 + nlen; 529 p[0] = CHAP_RESPONSE; 530 p[1] = id; 531 p[2] = len >> 8; 532 p[3] = len; 533 534 output(0, response, PPP_HDRLEN + len); 535} 536 537static void 538chap_handle_status(struct chap_client_state *cs, int code, int id, 539 unsigned char *pkt, int len) 540{ 541 const char *msg = NULL; 542 543 if ((cs->flags & (AUTH_DONE|AUTH_STARTED|LOWERUP)) 544 != (AUTH_STARTED|LOWERUP)) 545 return; 546 cs->flags |= AUTH_DONE; 547 548 UNTIMEOUT(chap_client_timeout, cs); 549 cs->flags &= ~TIMEOUT_PENDING; 550 551 if (code == CHAP_SUCCESS) { 552 /* used for MS-CHAP v2 mutual auth, yuck */ 553 if (cs->digest->check_success != NULL) { 554 if (!(*cs->digest->check_success)(id, pkt, len)) 555 code = CHAP_FAILURE; 556 } else 557 msg = "CHAP authentication succeeded"; 558 } else { 559 if (cs->digest->handle_failure != NULL) 560 (*cs->digest->handle_failure)(pkt, len); 561 else 562 msg = "CHAP authentication failed"; 563 } 564 if (msg) { 565 if (len > 0) 566 info("%s: %.*v", msg, len, pkt); 567 else 568 info("%s", msg); 569 } 570 if (code == CHAP_SUCCESS) 571 auth_withpeer_success(0, PPP_CHAP, cs->digest->code); 572 else { 573 cs->flags |= AUTH_FAILED; 574 error("CHAP authentication failed"); 575 auth_withpeer_fail(0, PPP_CHAP); 576 } 577} 578 579static void 580chap_input(int unit, unsigned char *pkt, int pktlen) 581{ 582 struct chap_client_state *cs = &client; 583 struct chap_server_state *ss = &server; 584 unsigned char code, id; 585 int len; 586 587 if (pktlen < CHAP_HDRLEN) 588 return; 589 GETCHAR(code, pkt); 590 GETCHAR(id, pkt); 591 GETSHORT(len, pkt); 592 if (len < CHAP_HDRLEN || len > pktlen) 593 return; 594 len -= CHAP_HDRLEN; 595 596 switch (code) { 597 case CHAP_CHALLENGE: 598 chap_respond(cs, id, pkt, len); 599 break; 600 case CHAP_RESPONSE: 601 chap_handle_response(ss, id, pkt, len); 602 break; 603 case CHAP_FAILURE: 604 case CHAP_SUCCESS: 605 chap_handle_status(cs, code, id, pkt, len); 606 break; 607 } 608} 609 610static void 611chap_protrej(int unit) 612{ 613 struct chap_client_state *cs = &client; 614 struct chap_server_state *ss = &server; 615 616 if (ss->flags & TIMEOUT_PENDING) { 617 ss->flags &= ~TIMEOUT_PENDING; 618 UNTIMEOUT(chap_server_timeout, ss); 619 } 620 if (ss->flags & AUTH_STARTED) { 621 ss->flags = 0; 622 auth_peer_fail(0, PPP_CHAP); 623 } 624 if ((cs->flags & (AUTH_STARTED|AUTH_DONE)) == AUTH_STARTED) { 625 cs->flags &= ~AUTH_STARTED; 626 error("CHAP authentication failed due to protocol-reject"); 627 auth_withpeer_fail(0, PPP_CHAP); 628 } 629} 630 631/* 632 * chap_print_pkt - print the contents of a CHAP packet. 633 */ 634static char *chap_code_names[] = { 635 "Challenge", "Response", "Success", "Failure" 636}; 637 638static int 639chap_print_pkt(unsigned char *p, int plen, 640 void (*printer)(void *, char *, ...), void *arg) 641{ 642 int code, id, len; 643 int clen, nlen; 644 unsigned char x; 645 646 if (plen < CHAP_HDRLEN) 647 return 0; 648 GETCHAR(code, p); 649 GETCHAR(id, p); 650 GETSHORT(len, p); 651 if (len < CHAP_HDRLEN || len > plen) 652 return 0; 653 654 if (code >= 1 && code <= sizeof(chap_code_names) / sizeof(char *)) 655 printer(arg, " %s", chap_code_names[code-1]); 656 else 657 printer(arg, " code=0x%x", code); 658 printer(arg, " id=0x%x", id); 659 len -= CHAP_HDRLEN; 660 switch (code) { 661 case CHAP_CHALLENGE: 662 case CHAP_RESPONSE: 663 if (len < 1) 664 break; 665 clen = p[0]; 666 if (len < clen + 1) 667 break; 668 ++p; 669 nlen = len - clen - 1; 670 printer(arg, " <"); 671 for (; clen > 0; --clen) { 672 GETCHAR(x, p); 673 printer(arg, "%.2x", x); 674 } 675 printer(arg, ">, name = "); 676 print_string((char *)p, nlen, printer, arg); 677 break; 678 case CHAP_FAILURE: 679 case CHAP_SUCCESS: 680 printer(arg, " "); 681 print_string((char *)p, len, printer, arg); 682 break; 683 default: 684 for (clen = len; clen > 0; --clen) { 685 GETCHAR(x, p); 686 printer(arg, " %.2x", x); 687 } 688 } 689 690 return len + CHAP_HDRLEN; 691} 692 693struct protent chap_protent = { 694 PPP_CHAP, 695 chap_init, 696 chap_input, 697 chap_protrej, 698 chap_lowerup, 699 chap_lowerdown, 700 NULL, /* open */ 701 NULL, /* close */ 702 chap_print_pkt, 703 NULL, /* datainput */ 704 1, /* enabled_flag */ 705 "CHAP", /* name */ 706 NULL, /* data_name */ 707 chap_option_list, 708 NULL, /* check_options */ 709}; 710