/* * utils/mountd/mountd.c * * Authenticate mount requests and retrieve file handle. * * Copyright (C) 1995, 1996 Olaf Kirch */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "xmalloc.h" #include "misc.h" #include "mountd.h" #include "rpcmisc.h" #include "pseudoflavors.h" extern void my_svc_run(void); static void usage(const char *, int exitcode); static exports get_exportlist(void); static struct nfs_fh_len *get_rootfh(struct svc_req *, dirpath *, nfs_export **, mountstat3 *, int v3); int reverse_resolve = 0; int new_cache = 0; int manage_gids; int use_ipaddr = -1; /* PRC: a high-availability callout program can be specified with -H * When this is done, the program will receive callouts whenever clients * send mount or unmount requests -- the callout is not needed for 2.6 kernel */ char *ha_callout_prog = NULL; /* Number of mountd threads to start. Default is 1 and * that's probably enough unless you need hundreds of * clients to be able to mount at once. */ static int num_threads = 1; /* Arbitrary limit on number of threads */ #define MAX_THREADS 64 static struct option longopts[] = { { "foreground", 0, 0, 'F' }, { "descriptors", 1, 0, 'o' }, { "debug", 1, 0, 'd' }, { "help", 0, 0, 'h' }, { "exports-file", 1, 0, 'f' }, { "nfs-version", 1, 0, 'V' }, { "no-nfs-version", 1, 0, 'N' }, { "version", 0, 0, 'v' }, { "port", 1, 0, 'p' }, { "no-tcp", 0, 0, 'n' }, { "ha-callout", 1, 0, 'H' }, { "state-directory-path", 1, 0, 's' }, { "num-threads", 1, 0, 't' }, { "reverse-lookup", 0, 0, 'r' }, { "manage-gids", 0, 0, 'g' }, { NULL, 0, 0, 0 } }; #define NFSVERSBIT(vers) (0x1 << (vers - 1)) #define NFSVERSBIT_ALL (NFSVERSBIT(2) | NFSVERSBIT(3) | NFSVERSBIT(4)) static int nfs_version = NFSVERSBIT_ALL; static int version2(void) { return nfs_version & NFSVERSBIT(2); } static int version3(void) { return nfs_version & NFSVERSBIT(3); } static int version23(void) { return nfs_version & (NFSVERSBIT(2) | NFSVERSBIT(3)); } static int version_any(void) { return nfs_version & NFSVERSBIT_ALL; } static void unregister_services (void) { nfs_svc_unregister(MOUNTPROG, MOUNTVERS); nfs_svc_unregister(MOUNTPROG, MOUNTVERS_POSIX); nfs_svc_unregister(MOUNTPROG, MOUNTVERS_NFSV3); } static void cleanup_lockfiles (void) { unlink(_PATH_XTABLCK); unlink(_PATH_ETABLCK); unlink(_PATH_RMTABLCK); } /* Wait for all worker child processes to exit and reap them */ static void wait_for_workers (void) { int status; pid_t pid; for (;;) { pid = waitpid(0, &status, 0); if (pid < 0) { if (errno == ECHILD) return; /* no more children */ xlog(L_FATAL, "mountd: can't wait: %s\n", strerror(errno)); } /* Note: because we SIG_IGN'd SIGCHLD earlier, this * does not happen on 2.6 kernels, and waitpid() blocks * until all the children are dead then returns with * -ECHILD. But, we don't need to do anything on the * death of individual workers, so we don't care. */ xlog(L_NOTICE, "mountd: reaped child %d, status %d\n", (int)pid, status); } } /* Fork num_threads worker children and wait for them */ static void fork_workers(void) { int i; pid_t pid; xlog(L_NOTICE, "mountd: starting %d threads\n", num_threads); for (i = 0 ; i < num_threads ; i++) { pid = fork(); if (pid < 0) { xlog(L_FATAL, "mountd: cannot fork: %s\n", strerror(errno)); } if (pid == 0) { /* worker child */ /* Re-enable the default action on SIGTERM et al * so that workers die naturally when sent them. * Only the parent unregisters with pmap and * hence needs to do special SIGTERM handling. */ struct sigaction sa; sa.sa_handler = SIG_DFL; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sigaction(SIGHUP, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); /* fall into my_svc_run in caller */ return; } } /* in parent */ wait_for_workers(); unregister_services(); cleanup_lockfiles(); xlog(L_NOTICE, "mountd: no more workers, exiting\n"); exit(0); } /* * Signal handler. */ static void killer (int sig) { unregister_services(); if (num_threads > 1) { /* play Kronos and eat our children */ kill(0, SIGTERM); wait_for_workers(); } cleanup_lockfiles(); xlog (L_NOTICE, "Caught signal %d, un-registering and exiting.", sig); exit(0); } static void sig_hup (int sig) { /* don't exit on SIGHUP */ xlog (L_NOTICE, "Received SIGHUP... Ignoring.\n", sig); return; } bool_t mount_null_1_svc(struct svc_req *rqstp, void *UNUSED(argp), void *UNUSED(resp)) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); char buf[INET6_ADDRSTRLEN]; xlog(D_CALL, "Received NULL request from %s", host_ntop(sap, buf, sizeof(buf))); return 1; } bool_t mount_mnt_1_svc(struct svc_req *rqstp, dirpath *path, fhstatus *res) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); char buf[INET6_ADDRSTRLEN]; struct nfs_fh_len *fh; xlog(D_CALL, "Received MNT1(%s) request from %s", *path, host_ntop(sap, buf, sizeof(buf))); fh = get_rootfh(rqstp, path, NULL, &res->fhs_status, 0); if (fh) memcpy(&res->fhstatus_u.fhs_fhandle, fh->fh_handle, 32); return 1; } bool_t mount_dump_1_svc(struct svc_req *rqstp, void *UNUSED(argp), mountlist *res) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); char buf[INET6_ADDRSTRLEN]; xlog(D_CALL, "Received DUMP request from %s", host_ntop(sap, buf, sizeof(buf))); *res = mountlist_list(); return 1; } bool_t mount_umnt_1_svc(struct svc_req *rqstp, dirpath *argp, void *UNUSED(resp)) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); nfs_export *exp; char *p = *argp; char rpath[MAXPATHLEN+1]; char buf[INET6_ADDRSTRLEN]; if (*p == '\0') p = "/"; if (realpath(p, rpath) != NULL) { rpath[sizeof (rpath) - 1] = '\0'; p = rpath; } xlog(D_CALL, "Received UMNT(%s) request from %s", p, host_ntop(sap, buf, sizeof(buf))); exp = auth_authenticate("unmount", sap, p); if (exp == NULL) return 1; mountlist_del(host_ntop(sap, buf, sizeof(buf)), p); return 1; } bool_t mount_umntall_1_svc(struct svc_req *rqstp, void *UNUSED(argp), void *UNUSED(resp)) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); char buf[INET6_ADDRSTRLEN]; xlog(D_CALL, "Received UMNTALL request from %s", host_ntop(sap, buf, sizeof(buf))); /* Reload /etc/xtab if necessary */ auth_reload(); mountlist_del_all(nfs_getrpccaller(rqstp->rq_xprt)); return 1; } bool_t mount_export_1_svc(struct svc_req *rqstp, void *UNUSED(argp), exports *resp) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); char buf[INET6_ADDRSTRLEN]; xlog(D_CALL, "Received EXPORT request from %s.", host_ntop(sap, buf, sizeof(buf))); *resp = get_exportlist(); return 1; } bool_t mount_exportall_1_svc(struct svc_req *rqstp, void *UNUSED(argp), exports *resp) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); char buf[INET6_ADDRSTRLEN]; xlog(D_CALL, "Received EXPORTALL request from %s.", host_ntop(sap, buf, sizeof(buf))); *resp = get_exportlist(); return 1; } /* * MNTv2 pathconf procedure * * The protocol doesn't include a status field, so Sun apparently considers * it good practice to let anyone snoop on your system, even if it's * pretty harmless data such as pathconf. We don't. * * Besides, many of the pathconf values don't make much sense on NFS volumes. * FIFOs and tty device files represent devices on the *client*, so there's * no point in getting the server's buffer sizes etc. */ bool_t mount_pathconf_2_svc(struct svc_req *rqstp, dirpath *path, ppathcnf *res) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); struct stat stb; nfs_export *exp; char rpath[MAXPATHLEN+1]; char *p = *path; char buf[INET6_ADDRSTRLEN]; memset(res, 0, sizeof(*res)); if (*p == '\0') p = "/"; /* Reload /etc/xtab if necessary */ auth_reload(); /* Resolve symlinks */ if (realpath(p, rpath) != NULL) { rpath[sizeof (rpath) - 1] = '\0'; p = rpath; } xlog(D_CALL, "Received PATHCONF(%s) request from %s", p, host_ntop(sap, buf, sizeof(buf))); /* Now authenticate the intruder... */ exp = auth_authenticate("pathconf", sap, p); if (exp == NULL) return 1; else if (stat(p, &stb) < 0) { xlog(L_WARNING, "can't stat exported dir %s: %s", p, strerror(errno)); return 1; } res->pc_link_max = pathconf(p, _PC_LINK_MAX); res->pc_max_canon = pathconf(p, _PC_MAX_CANON); res->pc_max_input = pathconf(p, _PC_MAX_INPUT); res->pc_name_max = pathconf(p, _PC_NAME_MAX); res->pc_path_max = pathconf(p, _PC_PATH_MAX); res->pc_pipe_buf = pathconf(p, _PC_PIPE_BUF); res->pc_vdisable = pathconf(p, _PC_VDISABLE); /* Can't figure out what to do with pc_mask */ res->pc_mask[0] = 0; res->pc_mask[1] = 0; return 1; } /* * We should advertise the preferred flavours first. (See RFC 2623 * section 2.7.) We leave that to the administrator, by advertising * flavours in the order they were listed in /etc/exports. AUTH_NULL is * dropped from the list to avoid backward compatibility issue with * older Linux clients, who inspect the list in reversed order. * * XXX: It might be more helpful to rearrange these so that flavors * giving more access (as determined from readonly and id-squashing * options) come first. (If we decide to do that we should probably do * that when reading the exports rather than here.) */ static void set_authflavors(struct mountres3_ok *ok, nfs_export *exp) { struct sec_entry *s; static int flavors[SECFLAVOR_COUNT]; int i = 0; for (s = exp->m_export.e_secinfo; s->flav; s++) { if (s->flav->fnum == AUTH_NULL) continue; flavors[i] = s->flav->fnum; i++; } if (i == 0) { /* default when there is no sec= option: */ i = 1; flavors[0] = AUTH_UNIX; } ok->auth_flavors.auth_flavors_val = flavors; ok->auth_flavors.auth_flavors_len = i; } /* * NFSv3 MOUNT procedure */ bool_t mount_mnt_3_svc(struct svc_req *rqstp, dirpath *path, mountres3 *res) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); struct mountres3_ok *ok = &res->mountres3_u.mountinfo; char buf[INET6_ADDRSTRLEN]; nfs_export *exp; struct nfs_fh_len *fh; xlog(D_CALL, "Received MNT3(%s) request from %s", *path, host_ntop(sap, buf, sizeof(buf))); fh = get_rootfh(rqstp, path, &exp, &res->fhs_status, 1); if (!fh) return 1; ok->fhandle.fhandle3_len = fh->fh_size; ok->fhandle.fhandle3_val = (char *)fh->fh_handle; set_authflavors(ok, exp); return 1; } static struct nfs_fh_len * get_rootfh(struct svc_req *rqstp, dirpath *path, nfs_export **expret, mountstat3 *error, int v3) { struct sockaddr *sap = nfs_getrpccaller(rqstp->rq_xprt); struct stat stb, estb; nfs_export *exp; struct nfs_fh_len *fh; char rpath[MAXPATHLEN+1]; char *p = *path; char buf[INET6_ADDRSTRLEN]; if (*p == '\0') p = "/"; /* Reload /var/lib/nfs/etab if necessary */ auth_reload(); /* Resolve symlinks */ if (realpath(p, rpath) != NULL) { rpath[sizeof (rpath) - 1] = '\0'; p = rpath; } /* Now authenticate the intruder... */ exp = auth_authenticate("mount", sap, p); if (exp == NULL) { *error = MNT3ERR_ACCES; return NULL; } if (stat(p, &stb) < 0) { xlog(L_WARNING, "can't stat exported dir %s: %s", p, strerror(errno)); if (errno == ENOENT) *error = MNT3ERR_NOENT; else *error = MNT3ERR_ACCES; return NULL; } if (!S_ISDIR(stb.st_mode) && !S_ISREG(stb.st_mode)) { xlog(L_WARNING, "%s is not a directory or regular file", p); *error = MNT3ERR_NOTDIR; return NULL; } if (stat(exp->m_export.e_path, &estb) < 0) { xlog(L_WARNING, "can't stat export point %s: %s", p, strerror(errno)); *error = MNT3ERR_NOENT; return NULL; } if (estb.st_dev != stb.st_dev && (!new_cache || !(exp->m_export.e_flags & NFSEXP_CROSSMOUNT))) { xlog(L_WARNING, "request to export directory %s below nearest filesystem %s", p, exp->m_export.e_path); *error = MNT3ERR_ACCES; return NULL; } if (exp->m_export.e_mountpoint && !is_mountpoint(exp->m_export.e_mountpoint[0]? exp->m_export.e_mountpoint: exp->m_export.e_path)) { xlog(L_WARNING, "request to export an unmounted filesystem: %s", p); *error = MNT3ERR_NOENT; return NULL; } if (new_cache) { /* This will be a static private nfs_export with just one * address. We feed it to kernel then extract the filehandle, * */ if (cache_export(exp, p)) { *error = MNT3ERR_ACCES; return NULL; } fh = cache_get_filehandle(exp, v3?64:32, p); if (fh == NULL) { *error = MNT3ERR_ACCES; return NULL; } } else { int did_export = 0; retry: if (exp->m_exported<1) { export_export(exp); did_export = 1; } if (!exp->m_xtabent) xtab_append(exp); if (v3) fh = getfh_size((struct sockaddr_in *)sap, p, 64); if (!v3 || (fh == NULL && errno == EINVAL)) { /* We first try the new nfs syscall. */ fh = getfh((struct sockaddr_in *)sap, p); if (fh == NULL && errno == EINVAL) /* Let's try the old one. */ fh = getfh_old((struct sockaddr_in *)sap, stb.st_dev, stb.st_ino); } if (fh == NULL && !did_export) { exp->m_exported = 0; goto retry; } if (fh == NULL) { xlog(L_WARNING, "getfh failed: %s", strerror(errno)); *error = MNT3ERR_ACCES; return NULL; } } *error = MNT_OK; mountlist_add(host_ntop(sap, buf, sizeof(buf)), p); if (expret) *expret = exp; return fh; } static void remove_all_clients(exportnode *e) { struct groupnode *g, *ng; for (g = e->ex_groups; g; g = ng) { ng = g->gr_next; xfree(g->gr_name); xfree(g); } e->ex_groups = NULL; } static void free_exportlist(exports *elist) { struct exportnode *e, *ne; for (e = *elist; e != NULL; e = ne) { ne = e->ex_next; remove_all_clients(e); xfree(e->ex_dir); xfree(e); } *elist = NULL; } static void prune_clients(nfs_export *exp, struct exportnode *e) { struct addrinfo *ai = NULL; struct groupnode *c, **cp; cp = &e->ex_groups; while ((c = *cp) != NULL) { if (client_gettype(c->gr_name) == MCL_FQDN && (ai = host_addrinfo(c->gr_name))) { if (client_check(exp->m_client, ai)) { *cp = c->gr_next; xfree(c->gr_name); xfree(c); freeaddrinfo(ai); continue; } freeaddrinfo(ai); } cp = &(c->gr_next); } } static exportnode *lookup_or_create_elist_entry(exports *elist, nfs_export *exp) { exportnode *e; for (e = *elist; e != NULL; e = e->ex_next) { if (!strcmp(exp->m_export.e_path, e->ex_dir)) return e; } e = xmalloc(sizeof(*e)); e->ex_next = *elist; e->ex_groups = NULL; e->ex_dir = xstrdup(exp->m_export.e_path); *elist = e; return e; } static void insert_group(struct exportnode *e, char *newname) { struct groupnode *g; for (g = e->ex_groups; g; g = g->gr_next) if (!strcmp(g->gr_name, newname)) return; g = xmalloc(sizeof(*g)); g->gr_name = xstrdup(newname); g->gr_next = e->ex_groups; e->ex_groups = g; } static exports get_exportlist(void) { static exports elist = NULL; struct exportnode *e; nfs_export *exp; int i; static unsigned int ecounter; unsigned int acounter; acounter = auth_reload(); if (elist && acounter == ecounter) return elist; ecounter = acounter; free_exportlist(&elist); for (i = 0; i < MCL_MAXTYPES; i++) { for (exp = exportlist[i].p_head; exp; exp = exp->m_next) { /* Don't show pseudo exports */ if (exp->m_export.e_flags & NFSEXP_V4ROOT) continue; e = lookup_or_create_elist_entry(&elist, exp); /* exports to "*" absorb any others */ if (i == MCL_ANONYMOUS && e->ex_groups) { remove_all_clients(e); continue; } /* non-FQDN's absorb FQDN's they contain: */ if (i != MCL_FQDN && e->ex_groups) prune_clients(exp, e); if (exp->m_export.e_hostname[0] != '\0') insert_group(e, exp->m_export.e_hostname); } } return elist; } int main(int argc, char **argv) { char *export_file = _PATH_EXPORTS; char *state_dir = NFS_STATEDIR; char *progname; unsigned int listeners = 0; int foreground = 0; int port = 0; int descriptors = 0; int c; int vers; struct sigaction sa; struct rlimit rlim; /* Set the basename */ if ((progname = strrchr(argv[0], '/')) != NULL) progname++; else progname = argv[0]; /* Parse the command line options and arguments. */ opterr = 0; while ((c = getopt_long(argc, argv, "o:nFd:f:p:P:hH:N:V:vrs:t:g", longopts, NULL)) != EOF) switch (c) { case 'g': manage_gids = 1; break; case 'o': descriptors = atoi(optarg); if (descriptors <= 0) { fprintf(stderr, "%s: bad descriptors: %s\n", progname, optarg); usage(progname, 1); } break; case 'F': foreground = 1; break; case 'd': xlog_sconfig(optarg, 1); break; case 'f': export_file = optarg; break; case 'H': /* PRC: specify a high-availability callout program */ ha_callout_prog = optarg; break; case 'h': usage(progname, 0); break; case 'P': /* XXX for nfs-server compatibility */ case 'p': port = atoi(optarg); if (port <= 0 || port > 65535) { fprintf(stderr, "%s: bad port number: %s\n", progname, optarg); usage(progname, 1); } break; case 'N': vers = atoi(optarg); if (vers < 2 || vers > 4) { fprintf(stderr, "%s: bad version number: %s\n", argv[0], optarg); usage(argv[0], 1); } nfs_version &= ~NFSVERSBIT(vers); break; case 'n': _rpcfdtype = SOCK_DGRAM; break; case 'r': reverse_resolve = 1; break; case 's': if ((state_dir = xstrdup(optarg)) == NULL) { fprintf(stderr, "%s: xstrdup(%s) failed!\n", progname, optarg); exit(1); } break; case 't': num_threads = atoi (optarg); break; case 'V': vers = atoi(optarg); if (vers < 2 || vers > 4) { fprintf(stderr, "%s: bad version number: %s\n", argv[0], optarg); usage(argv[0], 1); } nfs_version |= NFSVERSBIT(vers); break; case 'v': printf("%s version " VERSION "\n", progname); exit(0); case 0: break; case '?': default: usage(progname, 1); } /* No more arguments allowed. */ if (optind != argc || !version_any()) usage(progname, 1); if (chdir(state_dir)) { fprintf(stderr, "%s: chdir(%s) failed: %s\n", progname, state_dir, strerror(errno)); exit(1); } if (getrlimit (RLIMIT_NOFILE, &rlim) != 0) fprintf(stderr, "%s: getrlimit (RLIMIT_NOFILE) failed: %s\n", progname, strerror(errno)); else { /* glibc sunrpc code dies if getdtablesize > FD_SETSIZE */ if ((descriptors == 0 && rlim.rlim_cur > FD_SETSIZE) || descriptors > FD_SETSIZE) descriptors = FD_SETSIZE; if (descriptors) { rlim.rlim_cur = descriptors; if (setrlimit (RLIMIT_NOFILE, &rlim) != 0) { fprintf(stderr, "%s: setrlimit (RLIMIT_NOFILE) failed: %s\n", progname, strerror(errno)); exit(1); } } } /* Initialize logging. */ if (!foreground) xlog_stderr(0); xlog_open(progname); sa.sa_handler = SIG_IGN; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sigaction(SIGHUP, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGPIPE, &sa, NULL); /* WARNING: the following works on Linux and SysV, but not BSD! */ sigaction(SIGCHLD, &sa, NULL); /* Daemons should close all extra filehandles ... *before* RPC init. */ if (!foreground) closeall(3); new_cache = check_new_cache(); if (new_cache) cache_open(); unregister_services(); if (version2()) { listeners += nfs_svc_create("mountd", MOUNTPROG, MOUNTVERS, mount_dispatch, port); listeners += nfs_svc_create("mountd", MOUNTPROG, MOUNTVERS_POSIX, mount_dispatch, port); } if (version3()) listeners += nfs_svc_create("mountd", MOUNTPROG, MOUNTVERS_NFSV3, mount_dispatch, port); if (version23() && listeners == 0) xlog(L_FATAL, "mountd: could not create listeners\n"); sa.sa_handler = killer; sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sa.sa_handler = sig_hup; sigaction(SIGHUP, &sa, NULL); auth_init(export_file); if (!foreground) { /* We first fork off a child. */ if ((c = fork()) > 0) exit(0); if (c < 0) { xlog(L_FATAL, "mountd: cannot fork: %s\n", strerror(errno)); } /* Now we remove ourselves from the foreground. Redirect stdin/stdout/stderr first. */ { int fd = open("/dev/null", O_RDWR); (void) dup2(fd, 0); (void) dup2(fd, 1); (void) dup2(fd, 2); if (fd > 2) (void) close(fd); } setsid(); } /* silently bounds check num_threads */ if (foreground) num_threads = 1; else if (num_threads < 1) num_threads = 1; else if (num_threads > MAX_THREADS) num_threads = MAX_THREADS; if (num_threads > 1) fork_workers(); xlog(L_NOTICE, "Version " VERSION " starting"); my_svc_run(); xlog(L_ERROR, "RPC service loop terminated unexpectedly. Exiting...\n"); unregister_services(); exit(1); } static void usage(const char *prog, int n) { fprintf(stderr, "Usage: %s [-F|--foreground] [-h|--help] [-v|--version] [-d kind|--debug kind]\n" " [-o num|--descriptors num] [-f exports-file|--exports-file=file]\n" " [-p|--port port] [-V version|--nfs-version version]\n" " [-N version|--no-nfs-version version] [-n|--no-tcp]\n" " [-H ha-callout-prog] [-s|--state-directory-path path]\n" " [-g|--manage-gids] [-t num|--num-threads=num]\n", prog); exit(n); }