1/*
2 * Copyright (c) 2001-2019 Apple Inc. All rights reserved.
3 *
4 * @APPLE_OSREFERENCE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. The rights granted to you under the License
10 * may not be used to create, or enable the creation or redistribution of,
11 * unlawful or unlicensed copies of an Apple operating system, or to
12 * circumvent, violate, or enable the circumvention or violation of, any
13 * terms of an Apple operating system software license agreement.
14 *
15 * Please obtain a copy of the License at
16 * http://www.opensource.apple.com/apsl/ and read it before using this file.
17 *
18 * The Original Code and all software distributed under the License are
19 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
20 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
21 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
22 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
23 * Please see the License for the specific language governing rights and
24 * limitations under the License.
25 *
26 * @APPLE_OSREFERENCE_LICENSE_HEADER_END@
27 */
28
29/*
30 * History:
31 * 14 December, 2001 Dieter Siegmund (dieter@apple.com)
32 * - created
33 */
34#include <sys/param.h>
35#include <sys/systm.h>
36#include <sys/kernel.h>
37#include <sys/conf.h>
38#include <sys/ioctl.h>
39#include <sys/proc_internal.h>
40#include <sys/mount_internal.h>
41#include <sys/mbuf.h>
42#include <sys/filedesc.h>
43#include <sys/vnode_internal.h>
44#include <sys/malloc.h>
45#include <sys/socket.h>
46#include <sys/socketvar.h>
47#include <sys/reboot.h>
48#include <sys/kauth.h>
49#include <net/if.h>
50#include <net/if_dl.h>
51#include <net/if_types.h>
52#include <net/route.h>
53#include <netinet/in.h>
54#include <netinet/if_ether.h>
55#include <netinet/dhcp_options.h>
56
57#include <kern/kern_types.h>
58#include <kern/kalloc.h>
59#include <sys/netboot.h>
60#include <sys/imageboot.h>
61#include <pexpert/pexpert.h>
62
63extern int (*mountroot)(void);
64
65extern unsigned char rootdevice[];
66
67static int S_netboot = 0;
68static struct netboot_info * S_netboot_info_p;
69
70void *
71IOBSDRegistryEntryForDeviceTree(const char * path);
72
73void
74IOBSDRegistryEntryRelease(void * entry);
75
76const void *
77IOBSDRegistryEntryGetData(void * entry, const char * property_name,
78 int * packet_length);
79
80#define BOOTP_RESPONSE "bootp-response"
81#define BSDP_RESPONSE "bsdp-response"
82#define DHCP_RESPONSE "dhcp-response"
83
84#define IP_FORMAT "%d.%d.%d.%d"
85#define IP_CH(ip) ((u_char *)ip)
86#define IP_LIST(ip) IP_CH(ip)[0],IP_CH(ip)[1],IP_CH(ip)[2],IP_CH(ip)[3]
87
88#define kNetBootRootPathPrefixNFS "nfs:"
89#define kNetBootRootPathPrefixHTTP "http:"
90
91typedef enum {
92 kNetBootImageTypeUnknown = 0,
93 kNetBootImageTypeNFS = 1, // Deprecated
94 kNetBootImageTypeHTTP = 2,
95} NetBootImageType;
96
97struct netboot_info {
98 struct in_addr client_ip;
99 struct in_addr server_ip;
100 char * server_name;
101 size_t server_name_length;
102 char * mount_point;
103 size_t mount_point_length;
104 char * image_path;
105 size_t image_path_length;
106 NetBootImageType image_type;
107 char * second_image_path;
108 size_t second_image_path_length;
109};
110
111/*
112 * Function: parse_booter_path
113 * Purpose:
114 * Parse a string of the form:
115 * "<IP>:<host>:<mount>[:<image_path>]"
116 * into the given ip address, host, mount point, and optionally, image_path.
117 *
118 * Note:
119 * The passed in string is modified i.e. ':' is replaced by '\0'.
120 * Example:
121 * "17.202.16.17:seaport:/release/.images/Image9/CurrentHera"
122 */
123static __inline__ boolean_t
124parse_booter_path(char * path, struct in_addr * iaddr_p, char const * * host,
125 char * * mount_dir, char * * image_path)
126{
127 char * start;
128 char * colon;
129
130 /* IP address */
131 start = path;
132 colon = strchr(s: start, c: ':');
133 if (colon == NULL) {
134 return FALSE;
135 }
136 *colon = '\0';
137 if (inet_aton(start, iaddr_p) != 1) {
138 return FALSE;
139 }
140
141 /* host */
142 start = colon + 1;
143 colon = strchr(s: start, c: ':');
144 if (colon == NULL) {
145 return FALSE;
146 }
147 *colon = '\0';
148 *host = start;
149
150 /* mount */
151 start = colon + 1;
152 colon = strchr(s: start, c: ':');
153 *mount_dir = start;
154 if (colon == NULL) {
155 *image_path = NULL;
156 } else {
157 /* image path */
158 *colon = '\0';
159 start = colon + 1;
160 *image_path = start;
161 }
162 return TRUE;
163}
164
165/*
166 * Function: find_colon
167 * Purpose:
168 * Find the next unescaped instance of the colon character.
169 * If a colon is escaped (preceded by a backslash '\' character),
170 * shift the string over by one character to overwrite the backslash.
171 */
172static __inline__ char *
173find_colon(char * str)
174{
175 char * start = str;
176 char * colon;
177
178 while ((colon = strchr(s: start, c: ':')) != NULL) {
179 char * dst;
180 char * src;
181
182 if (colon == start) {
183 break;
184 }
185 if (colon[-1] != '\\') {
186 break;
187 }
188 for (dst = colon - 1, src = colon; *dst != '\0'; dst++, src++) {
189 *dst = *src;
190 }
191 start = colon;
192 }
193 return colon;
194}
195
196/*
197 * Function: parse_netboot_path
198 * Purpose:
199 * Parse a string of the form:
200 * "nfs:<IP>:<mount>[:<image_path>]"
201 * into the given ip address, host, mount point, and optionally, image_path.
202 * Notes:
203 * - the passed in string is modified i.e. ':' is replaced by '\0'
204 * - literal colons must be escaped with a backslash
205 *
206 * Examples:
207 * nfs:17.202.42.112:/Library/NetBoot/NetBootSP0:Jaguar/Jaguar.dmg
208 * nfs:17.202.42.112:/Volumes/Foo\:/Library/NetBoot/NetBootSP0:Jaguar/Jaguar.dmg
209 */
210static __inline__ boolean_t
211parse_netboot_path(char * path, struct in_addr * iaddr_p, char const * * host,
212 char * * mount_dir, char * * image_path)
213{
214 static char tmp[MAX_IPv4_STR_LEN]; /* Danger - not thread safe */
215 char * start;
216 char * colon;
217
218 if (strncmp(s1: path, kNetBootRootPathPrefixNFS,
219 n: strlen(kNetBootRootPathPrefixNFS)) != 0) {
220 return FALSE;
221 }
222
223 /* IP address */
224 start = path + strlen(kNetBootRootPathPrefixNFS);
225 colon = strchr(s: start, c: ':');
226 if (colon == NULL) {
227 return FALSE;
228 }
229 *colon = '\0';
230 if (inet_aton(start, iaddr_p) != 1) {
231 return FALSE;
232 }
233
234 /* mount point */
235 start = colon + 1;
236 colon = find_colon(str: start);
237 *mount_dir = start;
238 if (colon == NULL) {
239 *image_path = NULL;
240 } else {
241 /* image path */
242 *colon = '\0';
243 start = colon + 1;
244 (void)find_colon(str: start);
245 *image_path = start;
246 }
247 *host = inet_ntop(AF_INET, iaddr_p, tmp, sizeof(tmp));
248 return TRUE;
249}
250
251static boolean_t
252parse_image_path(char * path, struct in_addr * iaddr_p, char const * * host,
253 char * * mount_dir, char * * image_path)
254{
255 if (path[0] >= '0' && path[0] <= '9') {
256 return parse_booter_path(path, iaddr_p, host, mount_dir,
257 image_path);
258 }
259 return parse_netboot_path(path, iaddr_p, host, mount_dir,
260 image_path);
261}
262
263static boolean_t
264get_root_path(char * root_path)
265{
266 void * entry;
267 boolean_t found = FALSE;
268 const void * pkt;
269 int pkt_len;
270
271 entry = IOBSDRegistryEntryForDeviceTree(path: "/chosen");
272 if (entry == NULL) {
273 return FALSE;
274 }
275 pkt = IOBSDRegistryEntryGetData(entry, BSDP_RESPONSE, packet_length: &pkt_len);
276 if (pkt != NULL && pkt_len >= (int)sizeof(struct dhcp)) {
277 printf("netboot: retrieving root path from BSDP response\n");
278 } else {
279 pkt = IOBSDRegistryEntryGetData(entry, BOOTP_RESPONSE,
280 packet_length: &pkt_len);
281 if (pkt != NULL && pkt_len >= (int)sizeof(struct dhcp)) {
282 printf("netboot: retrieving root path from BOOTP response\n");
283 }
284 }
285 if (pkt != NULL) {
286 int len;
287 dhcpol_t options;
288 const char * path;
289 const struct dhcp * reply;
290
291 reply = (const struct dhcp *)pkt;
292 (void)dhcpol_parse_packet(options: &options, pkt: reply, len: pkt_len);
293
294 path = (const char *)dhcpol_find(list: &options,
295 tag: dhcptag_root_path_e, len_p: &len, NULL);
296 if (path) {
297 memcpy(dst: root_path, src: path, n: len);
298 root_path[len] = '\0';
299 found = TRUE;
300 }
301 }
302 IOBSDRegistryEntryRelease(entry);
303 return found;
304}
305
306static void
307save_path(char * * str_p, size_t * length_p, char * path)
308{
309 *length_p = strlen(s: path) + 1;
310 *str_p = kalloc_data(*length_p, Z_WAITOK);
311 strlcpy(dst: *str_p, src: path, n: *length_p);
312 return;
313}
314
315static struct netboot_info *
316netboot_info_init(struct in_addr iaddr)
317{
318 boolean_t have_root_path = FALSE;
319 struct netboot_info * info = NULL;
320 char * root_path = NULL;
321
322 info = (struct netboot_info *)kalloc_type(struct netboot_info, Z_WAITOK | Z_ZERO);
323 info->client_ip = iaddr;
324 info->image_type = kNetBootImageTypeUnknown;
325
326 /* check for a booter-specified path then a NetBoot path */
327 root_path = zalloc(view: ZV_NAMEI);
328
329 if (PE_parse_boot_argn(arg_string: "rp0", arg_ptr: root_path, MAXPATHLEN) == TRUE
330 || PE_parse_boot_argn(arg_string: "rp", arg_ptr: root_path, MAXPATHLEN) == TRUE
331 || PE_parse_boot_argn(arg_string: "rootpath", arg_ptr: root_path, MAXPATHLEN) == TRUE) {
332 if (imageboot_format_is_valid(root_path)) {
333 printf("netboot_info_init: rp0='%s' isn't a network path,"
334 " ignoring\n", root_path);
335 } else {
336 have_root_path = TRUE;
337 }
338 }
339 if (have_root_path == FALSE) {
340 have_root_path = get_root_path(root_path);
341 }
342 if (have_root_path) {
343 const char * server_name = NULL;
344 char * mount_point = NULL;
345 char * image_path = NULL;
346 struct in_addr server_ip;
347
348 if (parse_image_path(path: root_path, iaddr_p: &server_ip, host: &server_name,
349 mount_dir: &mount_point, image_path: &image_path)) {
350 /* kNetBootImageTypeNFS is deprecated */
351 printf("netboot: NFS boot is deprecated\n");
352 } else if (strncmp(s1: root_path, kNetBootRootPathPrefixHTTP,
353 n: strlen(kNetBootRootPathPrefixHTTP)) == 0) {
354 info->image_type = kNetBootImageTypeHTTP;
355 save_path(str_p: &info->image_path, length_p: &info->image_path_length,
356 path: root_path);
357 printf("netboot: HTTP URL %s\n", info->image_path);
358 } else {
359 printf("netboot: root path uses unrecognized format\n");
360 }
361
362 /* check for image-within-image */
363 if (info->image_path != NULL) {
364 if (PE_parse_boot_argn(IMAGEBOOT_ROOT_ARG, arg_ptr: root_path, MAXPATHLEN)
365 || PE_parse_boot_argn(arg_string: "rp1", arg_ptr: root_path, MAXPATHLEN)) {
366 /* rp1/root-dmg is the second-level image */
367 save_path(str_p: &info->second_image_path, length_p: &info->second_image_path_length,
368 path: root_path);
369 }
370 }
371 if (info->second_image_path != NULL) {
372 printf("netboot: nested image %s\n", info->second_image_path);
373 }
374 }
375 zfree(ZV_NAMEI, root_path);
376 return info;
377}
378
379static void
380netboot_info_free(struct netboot_info * * info_p)
381{
382 struct netboot_info * info = *info_p;
383
384 if (info) {
385 kfree_data(info->mount_point, info->mount_point_length);
386 kfree_data(info->server_name, info->server_name_length);
387 kfree_data(info->image_path, info->image_path_length);
388 kfree_data(info->second_image_path,
389 info->second_image_path_length);
390 kfree_type(struct netboot_info, info);
391 }
392 *info_p = NULL;
393}
394
395boolean_t
396netboot_iaddr(struct in_addr * iaddr_p)
397{
398 if (S_netboot_info_p == NULL) {
399 return FALSE;
400 }
401
402 *iaddr_p = S_netboot_info_p->client_ip;
403 return TRUE;
404}
405
406boolean_t
407netboot_rootpath(struct in_addr * server_ip,
408 char * name, size_t name_len,
409 char * path, size_t path_len)
410{
411 if (S_netboot_info_p == NULL) {
412 return FALSE;
413 }
414
415 name[0] = '\0';
416 path[0] = '\0';
417
418 if (S_netboot_info_p->mount_point_length == 0) {
419 return FALSE;
420 }
421 if (path_len < S_netboot_info_p->mount_point_length) {
422 printf("netboot: path too small %zu < %zu\n",
423 path_len, S_netboot_info_p->mount_point_length);
424 return FALSE;
425 }
426 strlcpy(dst: path, src: S_netboot_info_p->mount_point, n: path_len);
427 strlcpy(dst: name, src: S_netboot_info_p->server_name, n: name_len);
428 *server_ip = S_netboot_info_p->server_ip;
429 return TRUE;
430}
431
432
433static boolean_t
434get_ip_parameters(struct in_addr * iaddr_p, struct in_addr * netmask_p,
435 struct in_addr * router_p)
436{
437 void * entry;
438 const void * pkt;
439 int pkt_len;
440
441
442 entry = IOBSDRegistryEntryForDeviceTree(path: "/chosen");
443 if (entry == NULL) {
444 return FALSE;
445 }
446 pkt = IOBSDRegistryEntryGetData(entry, DHCP_RESPONSE, packet_length: &pkt_len);
447 if (pkt != NULL && pkt_len >= (int)sizeof(struct dhcp)) {
448 printf("netboot: retrieving IP information from DHCP response\n");
449 } else {
450 pkt = IOBSDRegistryEntryGetData(entry, BOOTP_RESPONSE, packet_length: &pkt_len);
451 if (pkt != NULL && pkt_len >= (int)sizeof(struct dhcp)) {
452 printf("netboot: retrieving IP information from BOOTP response\n");
453 }
454 }
455 if (pkt != NULL) {
456 const struct in_addr * ip;
457 int len;
458 dhcpol_t options;
459 const struct dhcp * reply;
460
461 reply = (const struct dhcp *)pkt;
462 (void)dhcpol_parse_packet(options: &options, pkt: reply, len: pkt_len);
463 *iaddr_p = reply->dp_yiaddr;
464 ip = (const struct in_addr *)
465 dhcpol_find(list: &options,
466 tag: dhcptag_subnet_mask_e, len_p: &len, NULL);
467 if (ip) {
468 *netmask_p = *ip;
469 }
470 ip = (const struct in_addr *)
471 dhcpol_find(list: &options, tag: dhcptag_router_e, len_p: &len, NULL);
472 if (ip) {
473 *router_p = *ip;
474 }
475 }
476 IOBSDRegistryEntryRelease(entry);
477 return pkt != NULL;
478}
479
480static int
481route_cmd(int cmd, struct in_addr d, struct in_addr g,
482 struct in_addr m, uint32_t more_flags, unsigned int ifscope)
483{
484 struct sockaddr_in dst;
485 int error;
486 uint32_t flags = RTF_UP | RTF_STATIC;
487 struct sockaddr_in gw;
488 struct sockaddr_in mask;
489
490 flags |= more_flags;
491
492 /* destination */
493 bzero(s: (caddr_t)&dst, n: sizeof(dst));
494 dst.sin_len = sizeof(dst);
495 dst.sin_family = AF_INET;
496 dst.sin_addr = d;
497
498 /* gateway */
499 bzero(s: (caddr_t)&gw, n: sizeof(gw));
500 gw.sin_len = sizeof(gw);
501 gw.sin_family = AF_INET;
502 gw.sin_addr = g;
503
504 /* mask */
505 bzero(s: &mask, n: sizeof(mask));
506 mask.sin_len = sizeof(mask);
507 mask.sin_family = AF_INET;
508 mask.sin_addr = m;
509
510 error = rtrequest_scoped(cmd, (struct sockaddr *)&dst,
511 (struct sockaddr *)&gw, (struct sockaddr *)&mask, flags, NULL, ifscope);
512
513 return error;
514}
515
516static int
517default_route_add(struct in_addr router, boolean_t proxy_arp)
518{
519 uint32_t flags = 0;
520 struct in_addr zeroes = { .s_addr = 0 };
521
522 if (proxy_arp == FALSE) {
523 flags |= RTF_GATEWAY;
524 }
525 return route_cmd(RTM_ADD, d: zeroes, g: router, m: zeroes, more_flags: flags, IFSCOPE_NONE);
526}
527
528static struct ifnet *
529find_interface(void)
530{
531 struct ifnet * ifp = NULL;
532
533 dlil_if_lock();
534 if (rootdevice[0]) {
535 ifp = ifunit((char *)rootdevice);
536 }
537 if (ifp == NULL) {
538 ifnet_head_lock_shared();
539 TAILQ_FOREACH(ifp, &ifnet_head, if_link)
540 if ((ifp->if_flags & (IFF_LOOPBACK | IFF_POINTOPOINT)) == 0) {
541 break;
542 }
543 ifnet_head_done();
544 }
545 dlil_if_unlock();
546 return ifp;
547}
548
549static const struct sockaddr_in blank_sin = {
550 .sin_len = sizeof(struct sockaddr_in),
551 .sin_family = AF_INET,
552 .sin_port = 0,
553 .sin_addr = { .s_addr = 0 },
554 .sin_zero = { 0, 0, 0, 0, 0, 0, 0, 0 }
555};
556
557static int
558inet_aifaddr(struct socket * so, const char * name,
559 const struct in_addr * addr,
560 const struct in_addr * mask,
561 const struct in_addr * broadcast)
562{
563 struct ifaliasreq ifra;
564
565 bzero(s: &ifra, n: sizeof(ifra));
566 strlcpy(dst: ifra.ifra_name, src: name, n: sizeof(ifra.ifra_name));
567 if (addr) {
568 *((struct sockaddr_in *)(void *)&ifra.ifra_addr) = blank_sin;
569 ((struct sockaddr_in *)(void *)&ifra.ifra_addr)->sin_addr = *addr;
570 }
571 if (mask) {
572 *((struct sockaddr_in *)(void *)&ifra.ifra_mask) = blank_sin;
573 ((struct sockaddr_in *)(void *)&ifra.ifra_mask)->sin_addr = *mask;
574 }
575 if (broadcast) {
576 *((struct sockaddr_in *)(void *)&ifra.ifra_broadaddr) = blank_sin;
577 ((struct sockaddr_in *)(void *)&ifra.ifra_broadaddr)->sin_addr = *broadcast;
578 }
579 return ifioctl(so, SIOCAIFADDR, (caddr_t)&ifra, current_proc());
580}
581
582
583int
584netboot_mountroot(void)
585{
586 int error = 0;
587 struct in_addr iaddr = { .s_addr = 0 };
588 struct ifreq ifr;
589 struct ifnet * ifp;
590 struct in_addr netmask = { .s_addr = 0 };
591 proc_t procp = current_proc();
592 struct in_addr router = { .s_addr = 0 };
593 struct socket * so = NULL;
594
595 bzero(s: &ifr, n: sizeof(ifr));
596
597 /* find the interface */
598 ifp = find_interface();
599 if (ifp == NULL) {
600 printf("netboot: no suitable interface\n");
601 error = ENXIO;
602 goto failed;
603 }
604 snprintf(ifr.ifr_name, count: sizeof(ifr.ifr_name), "%s", if_name(ifp));
605 printf("netboot: using network interface '%s'\n", ifr.ifr_name);
606
607 /* bring it up */
608 if ((error = socreate(AF_INET, aso: &so, SOCK_DGRAM, proto: 0)) != 0) {
609 printf("netboot: socreate, error=%d\n", error);
610 goto failed;
611 }
612 ifr.ifr_flags = ifp->if_flags | IFF_UP;
613 error = ifioctl(so, SIOCSIFFLAGS, (caddr_t)&ifr, procp);
614 if (error) {
615 printf("netboot: SIFFLAGS, error=%d\n", error);
616 goto failed;
617 }
618
619 /* grab information from the registry */
620 if (get_ip_parameters(iaddr_p: &iaddr, netmask_p: &netmask, router_p: &router) == FALSE) {
621 printf("netboot: can't retrieve IP parameters\n");
622 goto failed;
623 }
624 OS_ANALYZER_SUPPRESS("12641116") printf("netboot: IP address " IP_FORMAT, IP_LIST(&iaddr));
625 if (netmask.s_addr) {
626 printf(" netmask " IP_FORMAT, IP_LIST(&netmask));
627 }
628 if (router.s_addr) {
629 printf(" router " IP_FORMAT, IP_LIST(&router));
630 }
631 printf("\n");
632 error = inet_aifaddr(so, name: ifr.ifr_name, addr: &iaddr, mask: &netmask, NULL);
633 if (error) {
634 printf("netboot: inet_aifaddr failed, %d\n", error);
635 goto failed;
636 }
637 if (router.s_addr == 0) {
638 /* enable proxy arp if we don't have a router */
639 router.s_addr = iaddr.s_addr;
640 }
641 printf("netboot: adding default route " IP_FORMAT "\n",
642 IP_LIST(&router));
643 error = default_route_add(router, proxy_arp: router.s_addr == iaddr.s_addr);
644 if (error) {
645 printf("netboot: default_route_add failed %d\n", error);
646 }
647
648 soclose(so);
649
650 S_netboot_info_p = netboot_info_init(iaddr);
651 switch (S_netboot_info_p->image_type) {
652 default:
653 case kNetBootImageTypeNFS:
654 /* kNetBootImageTypeNFS is deprecated */
655 error = ENOTSUP;
656 break;
657 case kNetBootImageTypeHTTP:
658 error = netboot_setup();
659 break;
660 }
661 if (error == 0) {
662 S_netboot = 1;
663 } else {
664 S_netboot = 0;
665 }
666 return error;
667failed:
668 if (so != NULL) {
669 soclose(so);
670 }
671 return error;
672}
673
674int
675netboot_setup(void)
676{
677 int error = 0;
678
679 if (S_netboot_info_p == NULL
680 || S_netboot_info_p->image_path == NULL) {
681 goto done;
682 }
683 printf("netboot_setup: calling imageboot_mount_image\n");
684 error = imageboot_mount_image(root_path: S_netboot_info_p->image_path, height: -1, type: IMAGEBOOT_DMG);
685 if (error != 0) {
686 printf("netboot: failed to mount root image, %d\n", error);
687 } else if (S_netboot_info_p->second_image_path != NULL) {
688 error = imageboot_mount_image(root_path: S_netboot_info_p->second_image_path, height: 0, type: IMAGEBOOT_DMG);
689 if (error != 0) {
690 printf("netboot: failed to mount second root image, %d\n", error);
691 }
692 }
693
694done:
695 netboot_info_free(info_p: &S_netboot_info_p);
696 return error;
697}
698
699int
700netboot_root(void)
701{
702 return S_netboot;
703}
704