1 /*
   2  * CDDL HEADER START
   3  *
   4  * The contents of this file are subject to the terms of the
   5  * Common Development and Distribution License (the "License").
   6  * You may not use this file except in compliance with the License.
   7  *
   8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
   9  * or http://www.opensolaris.org/os/licensing.
  10  * See the License for the specific language governing permissions
  11  * and limitations under the License.
  12  *
  13  * When distributing Covered Code, include this CDDL HEADER in each
  14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
  15  * If applicable, add the following below this CDDL HEADER, with the
  16  * fields enclosed by brackets "[]" replaced with your own identifying
  17  * information: Portions Copyright [yyyy] [name of copyright owner]
  18  *
  19  * CDDL HEADER END
  20  */
  21 /*
  22  * Copyright (c) 1999, 2010, Oracle and/or its affiliates. All rights reserved.
  23  */
  24 
  25 #include <unistd.h>
  26 #include <sys/types.h>
  27 #include <sys/stat.h>
  28 #include <stdlib.h>
  29 #include <netinet/in.h>           /* struct in_addr */
  30 #include <netinet/dhcp.h>
  31 #include <signal.h>
  32 #include <sys/socket.h>
  33 #include <net/route.h>
  34 #include <net/if_arp.h>
  35 #include <string.h>
  36 #include <dhcpmsg.h>
  37 #include <ctype.h>
  38 #include <netdb.h>
  39 #include <fcntl.h>
  40 #include <stdio.h>
  41 #include <dhcp_hostconf.h>
  42 
  43 #include "states.h"
  44 #include "agent.h"
  45 #include "interface.h"
  46 #include "util.h"
  47 #include "packet.h"
  48 
  49 /*
  50  * this file contains utility functions that have no real better home
  51  * of their own.  they can largely be broken into six categories:
  52  *
  53  *  o  conversion functions -- functions to turn integers into strings,
  54  *     or to convert between units of a similar measure.
  55  *
  56  *  o  time and timer functions -- functions to handle time measurement
  57  *     and events.
  58  *
  59  *  o  ipc-related functions -- functions to simplify the generation of
  60  *     ipc messages to the agent's clients.
  61  *
  62  *  o  signal-related functions -- functions to clean up the agent when
  63  *     it receives a signal.
  64  *
  65  *  o  routing table manipulation functions
  66  *
  67  *  o  true miscellany -- anything else
  68  */
  69 
  70 /*
  71  * pkt_type_to_string(): stringifies a packet type
  72  *
  73  *   input: uchar_t: a DHCP packet type value, RFC 2131 or 3315
  74  *          boolean_t: B_TRUE if IPv6
  75  *  output: const char *: the stringified packet type
  76  */
  77 
  78 const char *
  79 pkt_type_to_string(uchar_t type, boolean_t isv6)
  80 {
  81         /*
  82          * note: the ordering in these arrays allows direct indexing of the
  83          *       table based on the RFC packet type value passed in.
  84          */
  85 
  86         static const char *v4types[] = {
  87                 "BOOTP",  "DISCOVER", "OFFER",   "REQUEST", "DECLINE",
  88                 "ACK",    "NAK",      "RELEASE", "INFORM"
  89         };
  90         static const char *v6types[] = {
  91                 NULL, "SOLICIT", "ADVERTISE", "REQUEST",
  92                 "CONFIRM", "RENEW", "REBIND", "REPLY",
  93                 "RELEASE", "DECLINE", "RECONFIGURE", "INFORMATION-REQUEST",
  94                 "RELAY-FORW", "RELAY-REPL"
  95         };
  96 
  97         if (isv6) {
  98                 if (type >= sizeof (v6types) / sizeof (*v6types) ||
  99                     v6types[type] == NULL)
 100                         return ("<unknown>");
 101                 else
 102                         return (v6types[type]);
 103         } else {
 104                 if (type >= sizeof (v4types) / sizeof (*v4types) ||
 105                     v4types[type] == NULL)
 106                         return ("<unknown>");
 107                 else
 108                         return (v4types[type]);
 109         }
 110 }
 111 
 112 /*
 113  * monosec_to_string(): converts a monosec_t into a date string
 114  *
 115  *   input: monosec_t: the monosec_t to convert
 116  *  output: const char *: the corresponding date string
 117  */
 118 
 119 const char *
 120 monosec_to_string(monosec_t monosec)
 121 {
 122         time_t  time = monosec_to_time(monosec);
 123         char    *time_string = ctime(&time);
 124 
 125         /* strip off the newline -- ugh, why, why, why.. */
 126         time_string[strlen(time_string) - 1] = '\0';
 127         return (time_string);
 128 }
 129 
 130 /*
 131  * monosec(): returns a monotonically increasing time in seconds that
 132  *            is not affected by stime(2) or adjtime(2).
 133  *
 134  *   input: void
 135  *  output: monosec_t: the number of seconds since some time in the past
 136  */
 137 
 138 monosec_t
 139 monosec(void)
 140 {
 141         return (gethrtime() / NANOSEC);
 142 }
 143 
 144 /*
 145  * monosec_to_time(): converts a monosec_t into real wall time
 146  *
 147  *    input: monosec_t: the absolute monosec_t to convert
 148  *   output: time_t: the absolute time that monosec_t represents in wall time
 149  */
 150 
 151 time_t
 152 monosec_to_time(monosec_t abs_monosec)
 153 {
 154         return (abs_monosec - monosec()) + time(NULL);
 155 }
 156 
 157 /*
 158  * hrtime_to_monosec(): converts a hrtime_t to monosec_t
 159  *
 160  *    input: hrtime_t: the time to convert
 161  *   output: monosec_t: the time in monosec_t
 162  */
 163 
 164 monosec_t
 165 hrtime_to_monosec(hrtime_t hrtime)
 166 {
 167         return (hrtime / NANOSEC);
 168 }
 169 
 170 /*
 171  * print_server_msg(): prints a message from a DHCP server
 172  *
 173  *   input: dhcp_smach_t *: the state machine the message is associated with
 174  *          const char *: the string to display
 175  *          uint_t: length of string
 176  *  output: void
 177  */
 178 
 179 void
 180 print_server_msg(dhcp_smach_t *dsmp, const char *msg, uint_t msglen)
 181 {
 182         if (msglen > 0) {
 183                 dhcpmsg(MSG_INFO, "%s: message from server: %.*s",
 184                     dsmp->dsm_name, msglen, msg);
 185         }
 186 }
 187 
 188 /*
 189  * alrm_exit(): Signal handler for SIGARLM. terminates grandparent.
 190  *
 191  *    input: int: signal the handler was called with.
 192  *
 193  *   output: void
 194  */
 195 
 196 static void
 197 alrm_exit(int sig)
 198 {
 199         int exitval;
 200 
 201         if (sig == SIGALRM && grandparent != 0)
 202                 exitval = EXIT_SUCCESS;
 203         else
 204                 exitval = EXIT_FAILURE;
 205 
 206         _exit(exitval);
 207 }
 208 
 209 /*
 210  * daemonize(): daemonizes the process
 211  *
 212  *   input: void
 213  *  output: int: 1 on success, 0 on failure
 214  */
 215 
 216 int
 217 daemonize(void)
 218 {
 219         /*
 220          * We've found that adoption takes sufficiently long that
 221          * a dhcpinfo run after dhcpagent -a is started may occur
 222          * before the agent is ready to process the request.
 223          * The result is an error message and an unhappy user.
 224          *
 225          * The initial process now sleeps for DHCP_ADOPT_SLEEP,
 226          * unless interrupted by a SIGALRM, in which case it
 227          * exits immediately. This has the effect that the
 228          * grandparent doesn't exit until the dhcpagent is ready
 229          * to process requests. This defers the the balance of
 230          * the system start-up script processing until the
 231          * dhcpagent is ready to field requests.
 232          *
 233          * grandparent is only set for the adopt case; other
 234          * cases do not require the wait.
 235          */
 236 
 237         if (grandparent != 0)
 238                 (void) signal(SIGALRM, alrm_exit);
 239 
 240         switch (fork()) {
 241 
 242         case -1:
 243                 return (0);
 244 
 245         case  0:
 246                 if (grandparent != 0)
 247                         (void) signal(SIGALRM, SIG_DFL);
 248 
 249                 /*
 250                  * setsid() makes us lose our controlling terminal,
 251                  * and become both a session leader and a process
 252                  * group leader.
 253                  */
 254 
 255                 (void) setsid();
 256 
 257                 /*
 258                  * under POSIX, a session leader can accidentally
 259                  * (through open(2)) acquire a controlling terminal if
 260                  * it does not have one.  just to be safe, fork again
 261                  * so we are not a session leader.
 262                  */
 263 
 264                 switch (fork()) {
 265 
 266                 case -1:
 267                         return (0);
 268 
 269                 case 0:
 270                         (void) signal(SIGHUP, SIG_IGN);
 271                         (void) chdir("/");
 272                         (void) umask(022);
 273                         closefrom(0);
 274                         break;
 275 
 276                 default:
 277                         _exit(EXIT_SUCCESS);
 278                 }
 279                 break;
 280 
 281         default:
 282                 if (grandparent != 0) {
 283                         (void) signal(SIGCHLD, SIG_IGN);
 284                         /*
 285                          * Note that we're not the agent here, so the DHCP
 286                          * logging subsystem hasn't been configured yet.
 287                          */
 288                         syslog(LOG_DEBUG | LOG_DAEMON, "dhcpagent: daemonize: "
 289                             "waiting for adoption to complete.");
 290                         if (sleep(DHCP_ADOPT_SLEEP) == 0) {
 291                                 syslog(LOG_WARNING | LOG_DAEMON,
 292                                     "dhcpagent: daemonize: timed out awaiting "
 293                                     "adoption.");
 294                         }
 295                         syslog(LOG_DEBUG | LOG_DAEMON, "dhcpagent: daemonize: "
 296                             "wait finished");
 297                 }
 298                 _exit(EXIT_SUCCESS);
 299         }
 300 
 301         return (1);
 302 }
 303 
 304 /*
 305  * update_default_route(): update the interface's default route
 306  *
 307  *   input: int: the type of message; either RTM_ADD or RTM_DELETE
 308  *          struct in_addr: the default gateway to use
 309  *          const char *: the interface associated with the route
 310  *          int: any additional flags (besides RTF_STATIC and RTF_GATEWAY)
 311  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
 312  */
 313 
 314 static boolean_t
 315 update_default_route(uint32_t ifindex, int type, struct in_addr *gateway_nbo,
 316     int flags)
 317 {
 318         struct {
 319                 struct rt_msghdr        rm_mh;
 320                 struct sockaddr_in      rm_dst;
 321                 struct sockaddr_in      rm_gw;
 322                 struct sockaddr_in      rm_mask;
 323                 struct sockaddr_dl      rm_ifp;
 324         } rtmsg;
 325 
 326         (void) memset(&rtmsg, 0, sizeof (rtmsg));
 327         rtmsg.rm_mh.rtm_version = RTM_VERSION;
 328         rtmsg.rm_mh.rtm_msglen  = sizeof (rtmsg);
 329         rtmsg.rm_mh.rtm_type    = type;
 330         rtmsg.rm_mh.rtm_pid     = getpid();
 331         rtmsg.rm_mh.rtm_flags   = RTF_GATEWAY | RTF_STATIC | flags;
 332         rtmsg.rm_mh.rtm_addrs   = RTA_GATEWAY | RTA_DST | RTA_NETMASK | RTA_IFP;
 333 
 334         rtmsg.rm_gw.sin_family  = AF_INET;
 335         rtmsg.rm_gw.sin_addr    = *gateway_nbo;
 336 
 337         rtmsg.rm_dst.sin_family = AF_INET;
 338         rtmsg.rm_dst.sin_addr.s_addr = htonl(INADDR_ANY);
 339 
 340         rtmsg.rm_mask.sin_family = AF_INET;
 341         rtmsg.rm_mask.sin_addr.s_addr = htonl(0);
 342 
 343         rtmsg.rm_ifp.sdl_family = AF_LINK;
 344         rtmsg.rm_ifp.sdl_index  = ifindex;
 345 
 346         return (write(rtsock_fd, &rtmsg, sizeof (rtmsg)) == sizeof (rtmsg));
 347 }
 348 
 349 /*
 350  * add_default_route(): add the default route to the given gateway
 351  *
 352  *   input: const char *: the name of the interface associated with the route
 353  *          struct in_addr: the default gateway to add
 354  *  output: boolean_t: B_TRUE on success, B_FALSE otherwise
 355  */
 356 
 357 boolean_t
 358 add_default_route(uint32_t ifindex, struct in_addr *gateway_nbo)
 359 {
 360         return (update_default_route(ifindex, RTM_ADD, gateway_nbo, RTF_UP));
 361 }
 362 
 363 /*
 364  * del_default_route(): deletes the default route to the given gateway
 365  *
 366  *   input: const char *: the name of the interface associated with the route
 367  *          struct in_addr: if not INADDR_ANY, the default gateway to remove
 368  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
 369  */
 370 
 371 boolean_t
 372 del_default_route(uint32_t ifindex, struct in_addr *gateway_nbo)
 373 {
 374         if (gateway_nbo->s_addr == htonl(INADDR_ANY)) /* no router */
 375                 return (B_TRUE);
 376 
 377         return (update_default_route(ifindex, RTM_DELETE, gateway_nbo, 0));
 378 }
 379 
 380 /*
 381  * inactivity_shutdown(): shuts down agent if there are no state machines left
 382  *                        to manage
 383  *
 384  *   input: iu_tq_t *: unused
 385  *          void *: unused
 386  *  output: void
 387  */
 388 
 389 /* ARGSUSED */
 390 void
 391 inactivity_shutdown(iu_tq_t *tqp, void *arg)
 392 {
 393         if (smach_count() > 0)       /* shouldn't happen, but... */
 394                 return;
 395 
 396         dhcpmsg(MSG_VERBOSE, "inactivity_shutdown: timed out");
 397 
 398         iu_stop_handling_events(eh, DHCP_REASON_INACTIVITY, NULL, NULL);
 399 }
 400 
 401 /*
 402  * graceful_shutdown(): shuts down the agent gracefully
 403  *
 404  *   input: int: the signal that caused graceful_shutdown to be called
 405  *  output: void
 406  */
 407 
 408 void
 409 graceful_shutdown(int sig)
 410 {
 411         iu_stop_handling_events(eh, (sig == SIGTERM ? DHCP_REASON_TERMINATE :
 412             DHCP_REASON_SIGNAL), drain_script, NULL);
 413 }
 414 
 415 /*
 416  * bind_sock(): binds a socket to a given IP address and port number
 417  *
 418  *   input: int: the socket to bind
 419  *          in_port_t: the port number to bind to, host byte order
 420  *          in_addr_t: the address to bind to, host byte order
 421  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
 422  */
 423 
 424 boolean_t
 425 bind_sock(int fd, in_port_t port_hbo, in_addr_t addr_hbo)
 426 {
 427         struct sockaddr_in      sin;
 428         int                     on = 1;
 429 
 430         (void) memset(&sin, 0, sizeof (struct sockaddr_in));
 431         sin.sin_family = AF_INET;
 432         sin.sin_port   = htons(port_hbo);
 433         sin.sin_addr.s_addr = htonl(addr_hbo);
 434 
 435         (void) setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (int));
 436 
 437         return (bind(fd, (struct sockaddr *)&sin, sizeof (sin)) == 0);
 438 }
 439 
 440 /*
 441  * bind_sock_v6(): binds a socket to a given IP address and port number
 442  *
 443  *   input: int: the socket to bind
 444  *          in_port_t: the port number to bind to, host byte order
 445  *          in6_addr_t: the address to bind to, network byte order
 446  *  output: boolean_t: B_TRUE on success, B_FALSE on failure
 447  */
 448 
 449 boolean_t
 450 bind_sock_v6(int fd, in_port_t port_hbo, const in6_addr_t *addr_nbo)
 451 {
 452         struct sockaddr_in6     sin6;
 453         int                     on = 1;
 454 
 455         (void) memset(&sin6, 0, sizeof (struct sockaddr_in6));
 456         sin6.sin6_family = AF_INET6;
 457         sin6.sin6_port   = htons(port_hbo);
 458         if (addr_nbo != NULL) {
 459                 (void) memcpy(&sin6.sin6_addr, addr_nbo,
 460                     sizeof (sin6.sin6_addr));
 461         }
 462 
 463         (void) setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (int));
 464 
 465         return (bind(fd, (struct sockaddr *)&sin6, sizeof (sin6)) == 0);
 466 }
 467 
 468 /*
 469  * valid_hostname(): check whether a string is a valid hostname
 470  *
 471  *   input: const char *: the string to verify as a hostname
 472  *  output: boolean_t: B_TRUE if the string is a valid hostname
 473  *
 474  * Note that we accept both host names beginning with a digit and
 475  * those containing hyphens.  Neither is strictly legal according
 476  * to the RFCs, but both are in common practice, so we endeavour
 477  * to not break what customers are using.
 478  */
 479 
 480 static boolean_t
 481 valid_hostname(const char *hostname)
 482 {
 483         unsigned int i;
 484 
 485         for (i = 0; hostname[i] != '\0'; i++) {
 486 
 487                 if (isalpha(hostname[i]) || isdigit(hostname[i]) ||
 488                     (((hostname[i] == '-') || (hostname[i] == '.')) && (i > 0)))
 489                         continue;
 490 
 491                 return (B_FALSE);
 492         }
 493 
 494         return (i > 0);
 495 }
 496 
 497 /*
 498  * iffile_to_hostname(): return the hostname contained on a line of the form
 499  *
 500  * [ ^I]*inet[ ^I]+hostname[\n]*\0
 501  *
 502  * in the file located at the specified path
 503  *
 504  *   input: const char *: the path of the file to look in for the hostname
 505  *  output: const char *: the hostname at that path, or NULL on failure
 506  */
 507 
 508 #define IFLINE_MAX      1024    /* maximum length of a hostname.<if> line */
 509 
 510 const char *
 511 iffile_to_hostname(const char *path)
 512 {
 513         FILE            *fp;
 514         static char     ifline[IFLINE_MAX];
 515 
 516         fp = fopen(path, "r");
 517         if (fp == NULL)
 518                 return (NULL);
 519 
 520         /*
 521          * /etc/hostname.<if> may contain multiple ifconfig commands, but each
 522          * such command is on a separate line (see the "while read ifcmds" code
 523          * in /etc/init.d/inetinit).  Thus we will read the file a line at a
 524          * time, searching for a line of the form
 525          *
 526          * [ ^I]*inet[ ^I]+hostname[\n]*\0
 527          *
 528          * extract the host name from it, and check it for validity.
 529          */
 530         while (fgets(ifline, sizeof (ifline), fp) != NULL) {
 531                 char *p;
 532 
 533                 if ((p = strstr(ifline, "inet")) != NULL) {
 534                         if ((p != ifline) && !isspace(p[-1])) {
 535                                 (void) fclose(fp);
 536                                 return (NULL);
 537                         }
 538                         p += 4; /* skip over "inet" and expect spaces or tabs */
 539                         if ((*p == '\n') || (*p == '\0')) {
 540                                 (void) fclose(fp);
 541                                 return (NULL);
 542                         }
 543                         if (isspace(*p)) {
 544                                 char *nlptr;
 545 
 546                                 /* no need to read more of the file */
 547                                 (void) fclose(fp);
 548 
 549                                 while (isspace(*p))
 550                                         p++;
 551                                 if ((nlptr = strrchr(p, '\n')) != NULL)
 552                                         *nlptr = '\0';
 553                                 if (strlen(p) > MAXHOSTNAMELEN) {
 554                                         dhcpmsg(MSG_WARNING,
 555                                             "iffile_to_hostname:"
 556                                             " host name too long");
 557                                         return (NULL);
 558                                 }
 559                                 if (valid_hostname(p)) {
 560                                         return (p);
 561                                 } else {
 562                                         dhcpmsg(MSG_WARNING,
 563                                             "iffile_to_hostname:"
 564                                             " host name not valid");
 565                                         return (NULL);
 566                                 }
 567                         } else {
 568                                 (void) fclose(fp);
 569                                 return (NULL);
 570                         }
 571                 }
 572         }
 573 
 574         (void) fclose(fp);
 575         return (NULL);
 576 }
 577 
 578 /*
 579  * init_timer(): set up a DHCP timer
 580  *
 581  *   input: dhcp_timer_t *: the timer to set up
 582  *  output: void
 583  */
 584 
 585 void
 586 init_timer(dhcp_timer_t *dt, lease_t startval)
 587 {
 588         dt->dt_id = -1;
 589         dt->dt_start = startval;
 590 }
 591 
 592 /*
 593  * cancel_timer(): cancel a DHCP timer
 594  *
 595  *   input: dhcp_timer_t *: the timer to cancel
 596  *  output: boolean_t: B_TRUE on success, B_FALSE otherwise
 597  */
 598 
 599 boolean_t
 600 cancel_timer(dhcp_timer_t *dt)
 601 {
 602         if (dt->dt_id == -1)
 603                 return (B_TRUE);
 604 
 605         if (iu_cancel_timer(tq, dt->dt_id, NULL) == 1) {
 606                 dt->dt_id = -1;
 607                 return (B_TRUE);
 608         }
 609 
 610         return (B_FALSE);
 611 }
 612 
 613 /*
 614  * schedule_timer(): schedule a DHCP timer.  Note that it must not be already
 615  *                   running, and that we can't cancel here.  If it were, and
 616  *                   we did, we'd leak a reference to the callback argument.
 617  *
 618  *   input: dhcp_timer_t *: the timer to schedule
 619  *  output: boolean_t: B_TRUE on success, B_FALSE otherwise
 620  */
 621 
 622 boolean_t
 623 schedule_timer(dhcp_timer_t *dt, iu_tq_callback_t *cbfunc, void *arg)
 624 {
 625         if (dt->dt_id != -1)
 626                 return (B_FALSE);
 627         dt->dt_id = iu_schedule_timer(tq, dt->dt_start, cbfunc, arg);
 628         return (dt->dt_id != -1);
 629 }
 630 
 631 /*
 632  * dhcpv6_status_code(): report on a DHCPv6 status code found in an option
 633  *                       buffer.
 634  *
 635  *   input: const dhcpv6_option_t *: pointer to option
 636  *          uint_t: option length
 637  *          const char **: error string (nul-terminated)
 638  *          const char **: message from server (unterminated)
 639  *          uint_t *: length of server message
 640  *  output: int: -1 on error, or >= 0 for a DHCPv6 status code
 641  */
 642 
 643 int
 644 dhcpv6_status_code(const dhcpv6_option_t *d6o, uint_t olen, const char **estr,
 645     const char **msg, uint_t *msglenp)
 646 {
 647         uint16_t status;
 648         static const char *v6_status[] = {
 649                 NULL,
 650                 "Unknown reason",
 651                 "Server has no addresses available",
 652                 "Client record unavailable",
 653                 "Prefix inappropriate for link",
 654                 "Client must use multicast",
 655                 "No prefix available"
 656         };
 657         static char sbuf[32];
 658 
 659         *estr = "";
 660         *msg = "";
 661         *msglenp = 0;
 662         if (d6o == NULL)
 663                 return (0);
 664         olen -= sizeof (*d6o);
 665         if (olen < 2) {
 666                 *estr = "garbled status code";
 667                 return (-1);
 668         }
 669 
 670         *msg = (const char *)(d6o + 1) + 2;
 671         *msglenp = olen - 2;
 672 
 673         (void) memcpy(&status, d6o + 1, sizeof (status));
 674         status = ntohs(status);
 675         if (status > 0) {
 676                 if (status > DHCPV6_STAT_NOPREFIX) {
 677                         (void) snprintf(sbuf, sizeof (sbuf), "status %u",
 678                             status);
 679                         *estr = sbuf;
 680                 } else {
 681                         *estr = v6_status[status];
 682                 }
 683         }
 684         return (status);
 685 }
 686 
 687 void
 688 write_lease_to_hostconf(dhcp_smach_t *dsmp)
 689 {
 690         PKT_LIST *plp[2];
 691         const char *hcfile;
 692 
 693         hcfile = ifname_to_hostconf(dsmp->dsm_name, dsmp->dsm_isv6);
 694         plp[0] = dsmp->dsm_ack;
 695         plp[1] = dsmp->dsm_orig_ack;
 696         if (write_hostconf(dsmp->dsm_name, plp, 2,
 697             monosec_to_time(dsmp->dsm_curstart_monosec),
 698             dsmp->dsm_isv6) != -1) {
 699                 dhcpmsg(MSG_DEBUG, "wrote lease to %s", hcfile);
 700         } else if (errno == EROFS) {
 701                 dhcpmsg(MSG_DEBUG, "%s is on a read-only file "
 702                     "system; not saving lease", hcfile);
 703         } else {
 704                 dhcpmsg(MSG_ERR, "cannot write %s (reboot will "
 705                     "not use cached configuration)", hcfile);
 706         }
 707 }