/* * Copyright (c) 1999-2002 Apple Computer, Inc. All rights reserved. * * @APPLE_LICENSE_HEADER_START@ * * The contents of this file constitute Original Code as defined in and * are subject to the Apple Public Source License Version 1.1 (the * "License"). You may not use this file except in compliance with the * License. Please obtain a copy of the License at * http://www.apple.com/publicsource and read it before using this file. * * This Original Code and all software distributed under the License are * distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT. Please see the * License for the specific language governing rights and limitations * under the License. * * @APPLE_LICENSE_HEADER_END@ */ /* * macNC.c * - macNC boot server * - supports Mac OS 9 AKA Classic netboot clients */ /* * Modification History: * * December 2, 1997 Dieter Siegmund (dieter@apple.com) * - created * February 1, 1999 Dieter Siegmund (dieter@apple.com) * - create sharepoints at init time (and anytime we get a SIGHUP) * and ensure permissions are correct * November 2, 2000 Dieter Siegmund (dieter@apple.com) * - removed code that creates sharepoints */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dhcp.h" #include "netinfo.h" #include "rfc_options.h" #import "subnetDescr.h" #include "interfaces.h" #include "bootpd.h" #include "bsdpd.h" #include "macnc_options.h" #include "macNC.h" #include "host_identifier.h" #include "NICache.h" #include "nbsp.h" #include "hfsvols.h" #include "nbimages.h" #include "AFPUsers.h" #include "NetBootServer.h" static __inline__ void S_timestamp(char * msg) { if (verbose) timestamp_syslog(msg); } static boolean_t set_privs_no_stat(u_char * path, struct stat * sb_p, uid_t uid, gid_t gid, mode_t mode, boolean_t lock) { boolean_t needs_chown = FALSE; boolean_t needs_chmod = FALSE; if (sb_p->st_uid != uid || sb_p->st_gid != gid) needs_chown = TRUE; if ((sb_p->st_mode & ACCESSPERMS) != mode) needs_chmod = TRUE; if (needs_chown || needs_chmod) { if (sb_p->st_flags & UF_IMMUTABLE) { if (chflags(path, 0) < 0) return (FALSE); } if (needs_chown) { if (chown(path, uid, gid) < 0) return (FALSE); } if (needs_chmod) { if (chmod(path, mode) < 0) return (FALSE); } if (lock) { if (chflags(path, UF_IMMUTABLE) < 0) return (FALSE); } } else if (lock) { if ((sb_p->st_flags & UF_IMMUTABLE) == 0) { if (chflags(path, UF_IMMUTABLE) < 0) return (FALSE); } } else if (sb_p->st_flags & UF_IMMUTABLE) { if (chflags(path, 0) < 0) return (FALSE); } return (TRUE); } boolean_t set_privs(u_char * path, struct stat * sb_p, uid_t uid, gid_t gid, mode_t mode, boolean_t lock) { if (stat(path, sb_p) != 0) { return (FALSE); } return (set_privs_no_stat(path, sb_p, uid, gid, mode, lock)); } /* * Function: S_set_uid_gid * * Purpose: * Given a path to a file, make the owner of both the * enclosing directory and the file itself to user/group uid/gid. */ static int S_set_uid_gid(u_char * file, uid_t uid, gid_t gid) { u_char dir[PATH_MAX]; u_char * last_slash = strrchr(file, '/'); if (file[0] != '/' || last_slash == NULL) { if (debug) printf("path '%s' is not valid\n", file); return (-1); } strncpy(dir, file, last_slash - file); dir[last_slash - file] = '\0'; if (chown(dir, uid, gid) == -1) return (-1); if (chown(file, uid, gid) == -1) return (-1); return (0); } /** ** Other local utility routines: **/ /* * Function: S_get_client_info * * Purpose: * Retrieve the macNC client information from the given packet. * First try to parse the dhcp options, then look for the client * version tag and client info tag. The client info tag will * contain "Apple MacNC". * * Returns: * TRUE and client_version if client version is present in packet * FALSE otherwise */ boolean_t macNC_get_client_info(struct dhcp * pkt, int pkt_size, dhcpol_t * options, u_int * client_version) { /* get the client version info - if not present, not an NC */ void * client_id; int opt_len; void * vers; vers = dhcpol_find(options, macNCtag_client_version_e, &opt_len, NULL); if (vers == NULL) return (FALSE); client_id = dhcpol_find(options, macNCtag_client_info_e, &opt_len, NULL); if (client_id == NULL) return (FALSE); if (opt_len != strlen(MACNC_CLIENT_INFO) || bcmp(client_id, MACNC_CLIENT_INFO, opt_len)) { return (FALSE); } if (client_version) *client_version = ntohl(*((unsigned long *)vers)); return (TRUE); } static __inline__ boolean_t S_make_finder_info(u_char * shadow_path, u_char * real_path) { return (hfs_copy_finder_info(shadow_path, real_path)); } /* * Function: S_get_volpath * * Purpose: * Format a volume pathname given a volume, directory and file name. */ static void S_get_volpath(u_char * path, NBSPEntry * entry, u_char * dir, u_char * file) { snprintf(path, PATH_MAX, "%s%s%s%s%s", entry->path, (dir && *dir) ? "/" : "", (dir && *dir) ? (char *)dir : "", (file && *file) ? "/" : "", (file && *file) ? (char *)file : ""); return; } /* * Function: S_create_volume_dir * * Purpose: * Create the given directory path on the given volume. */ static boolean_t S_create_volume_dir(NBSPEntry * entry, u_char * dirname, mode_t mode) { u_char path[PATH_MAX]; S_get_volpath(path, entry, dirname, NULL); if (create_path(path, mode) < 0) { my_log(LOG_INFO, "macNC: create_volume_dir: create_path(%s)" " failed, %m", path); return (FALSE); } (void)chmod(path, mode); return (TRUE); } /* * Function: S_create_shadow_file * * Purpose: * Create a new empty file with the given size and attributes * from another file. */ static boolean_t S_create_shadow_file(u_char * shadow_path, u_char * real_path, uid_t uid, gid_t gid, unsigned long long size) { int fd; S_set_uid_gid(shadow_path, ROOT_UID, 0); fd = open(shadow_path, O_CREAT | O_TRUNC | O_WRONLY, CLIENT_FILE_PERMS); if (fd < 0) { my_log(LOG_INFO, "macNC: couldn't create file '%s': %m", shadow_path); return (FALSE); } if (hfs_set_file_size(fd, size)) { my_log(LOG_INFO, "macNC: hfs_set_file_size '%s' failed: %m", shadow_path); goto err; } if (S_make_finder_info(shadow_path, real_path) == FALSE) goto err; fchmod(fd, CLIENT_FILE_PERMS); close(fd); /* correct the owner of the path */ if (S_set_uid_gid(shadow_path, uid, gid)) { my_log(LOG_INFO, "macNC: setuidgid '%s' to %ld,%ld failed: %m", shadow_path, uid, gid); return (FALSE); } return (TRUE); err: close(fd); return (FALSE); } static boolean_t S_add_afppath_option(struct in_addr servip, dhcpoa_t * options, NBSPEntry * entry, u_char * dir, u_char * file, int tag) { u_char buf[DHCP_OPTION_SIZE_MAX]; u_char err[256]; int len; u_char path[PATH_MAX]; if (dir && *dir) snprintf(path, sizeof(path), "%s/%s", dir, file); else { snprintf(path, sizeof(path), "%s", file); } len = sizeof(buf); if (macNCopt_encodeAFPPath(servip, AFP_PORT_NUMBER, entry->name, AFP_DIRID_NULL, AFP_PATHTYPE_LONG, path, '/', buf, &len, err) == FALSE) { my_log(LOG_INFO, "macNC: couldn't encode %s:%s, %s", entry->name, path, err); return (FALSE); } if (dhcpoa_add(options, tag, len, buf) != dhcpoa_success_e) { my_log(LOG_INFO, "macNC: couldn't add option %d failed: %s", tag, dhcpoa_err(options)); return (FALSE); } return (TRUE); } /* * Function: S_stat_path_vol_file * * Purpose: * Return the stat structure for the given volume/dir/file. */ static __inline__ int S_stat_path_vol_file(u_char * path, NBSPEntry * entry, u_char * dir, u_char * file, struct stat * sb_p) { S_get_volpath(path, entry, dir, file); return (stat(path, sb_p)); } static boolean_t S_freespace(u_char * path, unsigned long long * size) { struct statfs fsb; if (statfs(path, &fsb) != 0) { my_log(LOG_INFO, "macNC: statfs on '%s' failed %m", path); return (FALSE); } *size = ((unsigned long long)fsb.f_bavail) * ((unsigned long long)fsb.f_bsize); if (debug) printf("%s %lu x %lu = %qu bytes\n", path, fsb.f_bavail, fsb.f_bsize, *size); return (TRUE); } static NBSPEntry * S_find_volume_with_space(unsigned long long needspace, int def_vol_index) { unsigned long long freespace; int i; NBSPEntry * entry = NULL; u_char path[PATH_MAX]; int vol_index; for (i = 0, vol_index = def_vol_index; i < NBSPList_count(G_client_sharepoints); i++) { NBSPEntry * shp = NBSPList_element(G_client_sharepoints, vol_index); S_get_volpath(path, shp, NULL, NULL); if (S_freespace(path, &freespace) == TRUE) { #define SLOP_SPACE_BYTES (20 * 1024 * 1024) /* make sure there's some space left on the volume */ if (freespace >= (needspace + SLOP_SPACE_BYTES)) { entry = shp; if (debug) printf("selected volume %s\n", entry->name); break; /* out of for */ } } vol_index = (vol_index + 1) % NBSPList_count(G_client_sharepoints); } return (entry); } static boolean_t S_remove_shadow(u_char * shadow_path, NBSPEntry * entry, u_char * dir) { u_char path[PATH_MAX]; /* remove the shadow file */ S_set_uid_gid(shadow_path, ROOT_UID, 0); unlink(shadow_path); S_get_volpath(path, entry, dir, NULL); /* and its directory */ if (rmdir(path)) { u_char new_path[PATH_MAX]; if (debug) perror(path); S_get_volpath(new_path, entry, "Delete Me", NULL); /* couldn't delete it, try to rename it */ if (rename(path, new_path)) { return (FALSE); } } return (TRUE); } /* * Function: S_add_image_options * * Purpose: * Create/initialize image for client, format the paths into the * response options. */ static boolean_t S_add_image_options(NBImageEntryRef image_entry, uid_t uid, gid_t gid, struct in_addr servip, dhcpoa_t * options, int host_number, u_char * afp_hostname) { int def_vol_index; u_char dir_path[PATH_MAX]; struct stat dir_statb; int i; u_char nc_images_dir[PATH_MAX]; NBSPEntry * nc_volume = NULL; u_char path[PATH_MAX]; struct stat statb; int vol_index; /* make sure the bootfile exists and the permissions are correct */ snprintf(path, sizeof(path), "%s/%s/%s", image_entry->sharepoint.path, image_entry->dir_name, image_entry->bootfile); if (set_privs(path, &statb, ROOT_UID, G_admin_gid, SHARED_FILE_PERMS, TRUE) == FALSE) { syslog(LOG_INFO, "macNC: '%s' does not exist", path); return (FALSE); } snprintf(nc_images_dir, sizeof(nc_images_dir), "%s", afp_hostname); /* attempt to round-robin images across multiple volumes */ def_vol_index = (host_number - 1) % NBSPList_count(G_client_sharepoints); /* check all volumes for a client image directory starting at default */ nc_volume = NULL; for (i = 0, vol_index = def_vol_index; i < NBSPList_count(G_client_sharepoints); i++) { NBSPEntry * entry = NBSPList_element(G_client_sharepoints, vol_index); if (S_stat_path_vol_file(dir_path, entry, nc_images_dir, NULL, &dir_statb) == 0) { nc_volume = entry; break; } vol_index = (vol_index + 1) % NBSPList_count(G_client_sharepoints); } /* if the client has its own private copy of the image file, use it */ if (nc_volume != NULL && S_stat_path_vol_file(path, nc_volume, nc_images_dir, image_entry->type_info.classic.shared, &statb) == 0) { /* set the image file perms */ if (set_privs_no_stat(path, &statb, uid, gid, CLIENT_FILE_PERMS, FALSE) == FALSE) { my_log(LOG_INFO, "macNC: couldn't set permissions on path %s: %m", path); return (FALSE); } /* set the client dir perms */ if (set_privs_no_stat(dir_path, &dir_statb, uid, gid, CLIENT_DIR_PERMS, FALSE) == FALSE) { my_log(LOG_INFO, "macNC: couldn't set permissions on path %s: %m", dir_path); return (FALSE); } if (S_add_afppath_option(servip, options, nc_volume, nc_images_dir, image_entry->type_info.classic.shared, macNCtag_shared_system_file_e) == FALSE) { return (FALSE); } /* does the client have its own Private image? */ if (image_entry->type_info.classic.private != NULL) { if (S_stat_path_vol_file(path, nc_volume, nc_images_dir, image_entry->type_info.classic.private, &statb) == 0) { if (set_privs_no_stat(path, &statb, uid, gid, CLIENT_FILE_PERMS, FALSE) == FALSE) { my_log(LOG_INFO, "macNC: couldn't set permissions on path %s: %m", path); return (FALSE); } /* * We use macNCtag_page_file_e instead of * macNCtag_private_system_file_e as you would expect. * The reason is that the client ROM software assumes * that the private_system_file is read-only. It also * assumes that page_file is read-write. Since we don't * use page_file for anything else, we use that instead. * This is a hack/workaround. */ if (S_add_afppath_option(servip, options, nc_volume, nc_images_dir, image_entry->type_info.classic.private, macNCtag_page_file_e) == FALSE){ return (FALSE); } } } } else { /* client gets shadow file(s) */ unsigned long long needspace; u_char private_path[PATH_MAX]; u_char shadow_path[PATH_MAX]; u_char shared_path[PATH_MAX]; snprintf(shared_path, sizeof(shared_path), "%s/%s/%s", image_entry->sharepoint.path, image_entry->dir_name, image_entry->type_info.classic.shared); /* set the shared system image permissions */ if (set_privs(shared_path, &statb, ROOT_UID, G_admin_gid, SHARED_FILE_PERMS, TRUE) == FALSE) { syslog(LOG_INFO, "macNC: '%s' does not exist", shared_path); return (FALSE); } /* add the shared system image option */ if (S_add_afppath_option(servip, options, &image_entry->sharepoint, image_entry->dir_name, image_entry->type_info.classic.shared, macNCtag_shared_system_file_e) == FALSE) { return (FALSE); } if (image_entry->type_info.classic.private != NULL) { /* check for the private system image, set its permissions */ snprintf(private_path, sizeof(private_path), "%s/%s/%s", image_entry->sharepoint.path, image_entry->dir_name, image_entry->type_info.classic.private); if (set_privs(private_path, &statb, ROOT_UID, G_admin_gid, SHARED_FILE_PERMS, TRUE) == TRUE) { /* add the private image option */ if (S_add_afppath_option(servip, options, &image_entry->sharepoint, image_entry->dir_name, image_entry->type_info.classic.private, macNCtag_private_system_file_e) == FALSE) { return (FALSE); } } } #define ONE_MEG (1024UL * 1024UL) needspace = ((unsigned long long)G_shadow_size_meg) * ONE_MEG; if (nc_volume != NULL) { struct stat sb_shadow; boolean_t set_file_size = FALSE; boolean_t set_owner_perms = FALSE; S_get_volpath(shadow_path, nc_volume, nc_images_dir, kNetBootShadowName); if (stat(shadow_path, &sb_shadow) == 0) { /* shadow exists */ S_timestamp("shadow file exists"); if (debug) printf("shadow %qu need %qu\n", sb_shadow.st_size, needspace); if (sb_shadow.st_uid != uid || sb_shadow.st_gid != gid || (sb_shadow.st_mode & ACCESSPERMS) != CLIENT_FILE_PERMS) set_owner_perms = TRUE; if (sb_shadow.st_size < needspace) { unsigned long long difference; unsigned long long freespace = 0; set_file_size = TRUE; S_timestamp("shadow file needs to be grown"); /* check for enough space */ (void)S_freespace(shadow_path, &freespace); difference = (needspace - sb_shadow.st_size); if (freespace < difference) { my_log(LOG_INFO, "macNC: device full, " "attempting to relocate %s", shadow_path); /* blow away the shadow */ if (S_remove_shadow(shadow_path, nc_volume, nc_images_dir) == FALSE) { my_log(LOG_INFO, "macNC: couldn't remove" " shadow %s, %m", shadow_path); return (FALSE); } /* start fresh */ nc_volume = NULL; } } } else { /* start fresh */ if (S_remove_shadow(shadow_path, nc_volume, nc_images_dir) == FALSE) { my_log(LOG_INFO, "macNC: couldn't remove" " shadow %s, %m", shadow_path); return (FALSE); } nc_volume = NULL; } if (nc_volume != NULL) { if (set_file_size) { S_timestamp("setting shadow file size"); if (S_create_shadow_file(shadow_path, shared_path, uid, gid, needspace) == FALSE) { my_log(LOG_INFO, "macNC: couldn't create %s, %m", shadow_path); return (FALSE); } S_timestamp("shadow file size set"); } else if (set_owner_perms) { S_timestamp("setting shadow file perms/owner"); chmod(shadow_path, CLIENT_FILE_PERMS); S_set_uid_gid(shadow_path, uid, gid); S_timestamp("shadow file perms/owner set"); } } } if (nc_volume == NULL) { /* locate the client's image dir */ nc_volume = S_find_volume_with_space(needspace, def_vol_index); if (nc_volume == NULL) { if (G_disk_space_warned == FALSE) my_log(LOG_INFO, "macNC: can't create client image: " "OUT OF DISK SPACE"); G_disk_space_warned = TRUE; /* don't keep complaining */ return (FALSE); } S_get_volpath(shadow_path, nc_volume, nc_images_dir, kNetBootShadowName); G_disk_space_warned = FALSE; if (S_create_volume_dir(nc_volume, nc_images_dir, CLIENT_DIR_PERMS) == FALSE) { return (FALSE); } if (S_create_shadow_file(shadow_path, shared_path, uid, gid, needspace) == FALSE) { my_log(LOG_INFO, "macNC: couldn't create %s, %m", shadow_path); return (FALSE); } } /* add the shadow file option */ if (S_add_afppath_option(servip, options, nc_volume, nc_images_dir, kNetBootShadowName, macNCtag_shared_system_shadow_file_e) == FALSE) { return (FALSE); } } return (TRUE); } boolean_t macNC_allocate(NBImageEntryRef image_entry, struct dhcp * reply, u_char * hostname, struct in_addr servip, int host_number, dhcpoa_t * options, uid_t uid, u_char * afp_user, u_char * passwd) { if (dhcpoa_add(options, macNCtag_user_name_e, strlen(afp_user), afp_user) != dhcpoa_success_e) { my_log(LOG_INFO, "macNC: afp user name option add %s failed, %s", afp_user, dhcpoa_err(options)); return (FALSE); } /* add the Mac OS machine name option */ if (dhcpoa_add(options, macNCtag_MacOS_machine_name_e, strlen(hostname), hostname) != dhcpoa_success_e) { my_log(LOG_INFO, "macNC: machine name option add client %s failed, %s", hostname, dhcpoa_err(options)); return (FALSE); } { u_char buf[16]; int buf_len = sizeof(buf); if (macNCopt_str_to_type(passwd, macNCtype_afp_password_e, buf, &buf_len, NULL) == FALSE || dhcpoa_add(options, macNCtag_password_e, buf_len, buf) != dhcpoa_success_e) { my_log(LOG_INFO, "macNC: failed add afp password for %d", host_number); return (FALSE); } } if (S_add_image_options(image_entry, uid, G_admin_gid, servip, options, host_number, hostname) == FALSE) { my_log(LOG_INFO, "macNC: S_add_image_options for %s failed", afp_user); return (FALSE); } return (TRUE); } void macNC_unlink_shadow(int host_number, u_char * hostname) { int def_vol_index; int i; u_char nc_images_dir[PATH_MAX]; NBSPEntry * nc_volume = NULL; struct stat shadow_statb; u_char shadow_path[PATH_MAX]; int vol_index; snprintf(nc_images_dir, sizeof(nc_images_dir), "%s", hostname); def_vol_index = (host_number - 1) % NBSPList_count(G_client_sharepoints); /* check all volumes for a client image directory starting at default */ nc_volume = NULL; for (i = 0, vol_index = def_vol_index; i < NBSPList_count(G_client_sharepoints); i++) { NBSPEntry * entry = NBSPList_element(G_client_sharepoints, vol_index); if (S_stat_path_vol_file(shadow_path, entry, nc_images_dir, kNetBootShadowName, &shadow_statb) == 0) { if (unlink(shadow_path) < 0) { my_log(LOG_DEBUG, "macNC: unlink(%s) failed, %m", shadow_path); } return; } vol_index = (vol_index + 1) % NBSPList_count(G_client_sharepoints); } return; }