/* $NetBSD: dlz_filesystem_driver.c,v 1.8 2023/01/25 21:43:28 christos Exp $ */ /* * Copyright (C) 2002 Stichting NLnet, Netherlands, stichting@nlnet.nl. * Copyright (C) Internet Systems Consortium, Inc. ("ISC") * * SPDX-License-Identifier: MPL-2.0 * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at https://mozilla.org/MPL/2.0/. * * See the COPYRIGHT file distributed with this work for additional * information regarding copyright ownership. */ /* * Copyright (C) 1999-2001, 2016 Internet Systems Consortium, Inc. ("ISC") * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifdef DLZ_FILESYSTEM #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static dns_sdlzimplementation_t *dlz_fs = NULL; typedef struct config_data { char *basedir; int basedirsize; char *datadir; int datadirsize; char *xfrdir; int xfrdirsize; int splitcnt; char separator; char pathsep; isc_mem_t *mctx; } config_data_t; typedef struct dir_entry dir_entry_t; struct dir_entry { char dirpath[PATH_MAX]; ISC_LINK(dir_entry_t) link; }; typedef ISC_LIST(dir_entry_t) dlist_t; /* forward reference */ static void fs_destroy(void *driverarg, void *dbdata); /* * Private methods */ static bool is_safe(const char *input) { unsigned int i; unsigned int len = strlen(input); /* check that only allowed characters are in the domain name */ for (i = 0; i < len; i++) { /* '.' is allowed, but has special requirements */ if (input[i] == '.') { /* '.' is not allowed as first char */ if (i == 0) { return (false); } /* '..', two dots together is not allowed. */ if (input[i - 1] == '.') { return (false); } /* '.' is not allowed as last char */ if (i == len - 1) { return (false); } /* only 1 dot in ok location, continue at next char */ continue; } /* '-' is allowed, continue at next char */ if (input[i] == '-') { continue; } /* 0-9 is allowed, continue at next char */ if (input[i] >= '0' && input[i] <= '9') { continue; } /* A-Z uppercase is allowed, continue at next char */ if (input[i] >= 'A' && input[i] <= 'Z') { continue; } /* a-z lowercase is allowed, continue at next char */ if (input[i] >= 'a' && input[i] <= 'z') { continue; } /* * colon needs to be allowed for IPV6 client * addresses. Not dangerous in domain names, as not a * special char. */ if (input[i] == ':') { continue; } /* * '@' needs to be allowed for in zone data. Not * dangerous in domain names, as not a special char. */ if (input[i] == '@') { continue; } /* * if we reach this point we have encountered a * disallowed char! */ return (false); } /* everything ok. */ return (true); } static void create_path_helper(char *out, const char *in, config_data_t *cd) { char *tmpString; char *tmpPtr; int i; tmpString = isc_mem_strdup(named_g_mctx, in); /* * don't forget is_safe guarantees '.' will NOT be the * first/last char */ while ((tmpPtr = strrchr(tmpString, '.')) != NULL) { i = 0; while (tmpPtr[i + 1] != '\0') { if (cd->splitcnt < 1) { strcat(out, (char *)&tmpPtr[i + 1]); } else { strncat(out, (char *)&tmpPtr[i + 1], cd->splitcnt); } strncat(out, (char *)&cd->pathsep, 1); if (cd->splitcnt == 0) { break; } if (strlen((char *)&tmpPtr[i + 1]) <= (unsigned int)cd->splitcnt) { break; } i += cd->splitcnt; } tmpPtr[0] = '\0'; } /* handle the "first" label properly */ i = 0; tmpPtr = tmpString; while (tmpPtr[i] != '\0') { if (cd->splitcnt < 1) { strcat(out, (char *)&tmpPtr[i]); } else { strncat(out, (char *)&tmpPtr[i], cd->splitcnt); } strncat(out, (char *)&cd->pathsep, 1); if (cd->splitcnt == 0) { break; } if (strlen((char *)&tmpPtr[i]) <= (unsigned int)cd->splitcnt) { break; } i += cd->splitcnt; } isc_mem_free(named_g_mctx, tmpString); } /*% * Checks to make sure zone and host are safe. If safe, then * hashes zone and host strings to build a path. If zone / host * are not safe an error is returned. */ static isc_result_t create_path(const char *zone, const char *host, const char *client, config_data_t *cd, char **path) { char *tmpPath; int pathsize; int len; bool isroot = false; /* we require a zone & cd parameter */ REQUIRE(zone != NULL); REQUIRE(cd != NULL); /* require path to be a pointer to NULL */ REQUIRE(path != NULL && *path == NULL); /* * client and host may both be NULL, but they can't both be * NON-NULL */ REQUIRE((host == NULL && client == NULL) || (host != NULL && client == NULL) || (host == NULL && client != NULL)); /* special case for root zone */ if (strcmp(zone, ".") == 0) { isroot = true; } /* if the requested zone is "unsafe", return error */ if (!isroot && !is_safe(zone)) { return (ISC_R_FAILURE); } /* if host was passed, verify that it is safe */ if (host != NULL && !is_safe(host)) { return (ISC_R_FAILURE); } /* if client was passed, verify that it is safe */ if (client != NULL && !is_safe(client)) { return (ISC_R_FAILURE); } /* Determine how much memory the split up string will require */ if (host != NULL) { len = strlen(zone) + strlen(host); } else if (client != NULL) { len = strlen(zone) + strlen(client); } else { len = strlen(zone); } /* * even though datadir and xfrdir will never be in the same * string we only waste a few bytes by allocating for both, * and then we are safe from buffer overruns. */ pathsize = len + cd->basedirsize + cd->datadirsize + cd->xfrdirsize + 4; /* if we are splitting names, we will need extra space. */ if (cd->splitcnt > 0) { pathsize += len / cd->splitcnt; } tmpPath = isc_mem_allocate(named_g_mctx, pathsize * sizeof(char)); /* * build path string. * start out with base directory. */ strcpy(tmpPath, cd->basedir); /* add zone name - parsed properly */ if (!isroot) { create_path_helper(tmpPath, zone, cd); } /* * When neither client or host is passed we are building a * path to see if a zone is supported. We require that a zone * path have the "data dir" directory contained within it so * that we know this zone is really supported. Otherwise, * this zone may not really be supported because we are * supporting a delagated sub zone. * * Example: * * We are supporting long.domain.com and using a splitcnt of * 0. the base dir is "/base-dir/" and the data dir is * "/.datadir" We want to see if we are authoritative for * domain.com. Path /base-dir/com/domain/.datadir since * /base-dir/com/domain/.datadir does not exist, we are not * authoritative for the domain "domain.com". However we are * authoritative for the domain "long.domain.com" because the * path /base-dir/com/domain/long/.datadir does exist! */ /* if client is passed append xfr dir, otherwise append data dir */ if (client != NULL) { strcat(tmpPath, cd->xfrdir); strncat(tmpPath, (char *)&cd->pathsep, 1); strcat(tmpPath, client); } else { strcat(tmpPath, cd->datadir); } /* if host not null, add it. */ if (host != NULL) { strncat(tmpPath, (char *)&cd->pathsep, 1); create_path_helper(tmpPath, host, cd); } /* return the path we built. */ *path = tmpPath; return (ISC_R_SUCCESS); } static isc_result_t process_dir(isc_dir_t *dir, void *passback, config_data_t *cd, dlist_t *dir_list, unsigned int basedirlen) { char tmp[PATH_MAX + NAME_MAX]; int astPos; struct stat sb; isc_result_t result = ISC_R_FAILURE; char *endp; char *type; char *ttlStr; char *data; char host[NAME_MAX]; char *tmpString; char *tmpPtr; int ttl; int i; int len; dir_entry_t *direntry; bool foundHost; tmp[0] = '\0'; /* set 1st byte to '\0' so strcpy works right. */ host[0] = '\0'; foundHost = false; /* copy base directory name to tmp. */ strcpy(tmp, dir->dirname); /* dir->dirname will always have '*' as the last char. */ astPos = strlen(dir->dirname) - 1; /* if dir_list != NULL, were are performing a zone xfr */ if (dir_list != NULL) { /* if splitcnt == 0, determine host from path. */ if (cd->splitcnt == 0) { if (strlen(tmp) - 3 > basedirlen) { tmp[astPos - 1] = '\0'; tmpString = (char *)&tmp[basedirlen + 1]; /* handle filesystem's special wildcard "-" */ if (strcmp(tmpString, "-") == 0) { strcpy(host, "*"); } else { /* * not special wildcard -- normal name */ while ((tmpPtr = strrchr( tmpString, cd->pathsep)) != NULL) { if ((strlen(host) + strlen(tmpPtr + 1) + 2) > NAME_MAX) { continue; } strcat(host, tmpPtr + 1); strcat(host, "."); tmpPtr[0] = '\0'; } if ((strlen(host) + strlen(tmpString) + 1) <= NAME_MAX) { strcat(host, tmpString); } } foundHost = true; /* set tmp again for use later */ strcpy(tmp, dir->dirname); } } else { /* * if splitcnt != 0 determine host from * ".host" directory entry */ while (isc_dir_read(dir) == ISC_R_SUCCESS) { if (strncasecmp(".host", dir->entry.name, 5) == 0) { /* * handle filesystem's special * wildcard "-" */ if (strcmp((char *)&dir->entry.name[6], "-") == 0) { strlcpy(host, "*", sizeof(host)); } else { strlcpy(host, (char *)&dir->entry .name[6], sizeof(host)); } foundHost = true; break; } } /* reset dir list for use later */ isc_dir_reset(dir); } /* end of else */ } while (isc_dir_read(dir) == ISC_R_SUCCESS) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_DEBUG(1), "Filesystem driver Dir name:" " '%s' Dir entry: '%s'\n", dir->dirname, dir->entry.name); /* skip any entries starting with "." */ if (dir->entry.name[0] == '.') { continue; } /* * get rid of '*', set to NULL. Effectively trims * string from previous loop to base directory only * while still leaving memory for concat to be * performed next. */ tmp[astPos] = '\0'; /* add name to base directory name. */ strcat(tmp, dir->entry.name); /* make sure we can stat entry */ if (stat(tmp, &sb) == 0) { /* if entry is a directory */ if ((sb.st_mode & S_IFDIR) != 0) { /* * if dir list is NOT NULL, add dir to * dir list */ if (dir_list != NULL) { direntry = isc_mem_get( named_g_mctx, sizeof(dir_entry_t)); strcpy(direntry->dirpath, tmp); ISC_LINK_INIT(direntry, link); ISC_LIST_APPEND(*dir_list, direntry, link); result = ISC_R_SUCCESS; } continue; /* * if entry is a file be sure we do * not add entry to DNS results if we * are performing a zone xfr and we * could not find a host entry. */ } else if (dir_list != NULL && !foundHost) { continue; } } else { /* if we cannot stat entry, skip it. */ continue; } type = dir->entry.name; ttlStr = strchr(type, cd->separator); if (ttlStr == NULL) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Filesystem driver: " "%s could not be parsed properly", tmp); return (ISC_R_FAILURE); } /* replace separator char with NULL to split string */ ttlStr[0] = '\0'; /* start string after NULL of previous string */ ttlStr = (char *)&ttlStr[1]; data = strchr(ttlStr, cd->separator); if (data == NULL) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Filesystem driver: " "%s could not be parsed properly", tmp); return (ISC_R_FAILURE); } /* replace separator char with NULL to split string */ data[0] = '\0'; /* start string after NULL of previous string */ data = (char *)&data[1]; /* replace all cd->separator chars with a space. */ len = strlen(data); for (i = 0; i < len; i++) { if (data[i] == cd->separator) { data[i] = ' '; } } /* convert text to int, make sure it worked right */ ttl = strtol(ttlStr, &endp, 10); if (*endp != '\0' || ttl < 0) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Filesystem driver " "ttl must be a positive number"); } /* pass data back to Bind */ if (dir_list == NULL) { result = dns_sdlz_putrr((dns_sdlzlookup_t *)passback, type, ttl, data); } else { result = dns_sdlz_putnamedrr( (dns_sdlzallnodes_t *)passback, (char *)host, type, ttl, data); } /* if error, return error right away */ if (result != ISC_R_SUCCESS) { return (result); } } /* end of while loop */ return (result); } /* * SDLZ interface methods */ static isc_result_t fs_allowzonexfr(void *driverarg, void *dbdata, const char *name, const char *client) { isc_result_t result; char *path; struct stat sb; config_data_t *cd; path = NULL; UNUSED(driverarg); cd = (config_data_t *)dbdata; if (create_path(name, NULL, client, cd, &path) != ISC_R_SUCCESS) { return (ISC_R_NOTFOUND); } if (stat(path, &sb) != 0) { result = ISC_R_NOTFOUND; goto complete_AXFR; } if ((sb.st_mode & S_IFREG) != 0) { result = ISC_R_SUCCESS; goto complete_AXFR; } result = ISC_R_NOTFOUND; complete_AXFR: isc_mem_free(named_g_mctx, path); return (result); } static isc_result_t fs_allnodes(const char *zone, void *driverarg, void *dbdata, dns_sdlzallnodes_t *allnodes) { isc_result_t result; dlist_t *dir_list; config_data_t *cd; char *basepath; unsigned int basepathlen; struct stat sb; isc_dir_t dir; dir_entry_t *dir_entry; dir_entry_t *next_de; basepath = NULL; dir_list = NULL; UNUSED(driverarg); UNUSED(allnodes); cd = (config_data_t *)dbdata; /* allocate memory for list */ dir_list = isc_mem_get(named_g_mctx, sizeof(dlist_t)); /* initialize list */ ISC_LIST_INIT(*dir_list); if (create_path(zone, NULL, NULL, cd, &basepath) != ISC_R_SUCCESS) { result = ISC_R_NOTFOUND; goto complete_allnds; } /* remove path separator at end of path so stat works properly */ basepathlen = strlen(basepath); if (stat(basepath, &sb) != 0) { result = ISC_R_NOTFOUND; goto complete_allnds; } if ((sb.st_mode & S_IFDIR) == 0) { result = ISC_R_NOTFOUND; goto complete_allnds; } /* initialize and open directory */ isc_dir_init(&dir); result = isc_dir_open(&dir, basepath); /* if directory open failed, return error. */ if (result != ISC_R_SUCCESS) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Unable to open %s directory to read entries.", basepath); result = ISC_R_FAILURE; goto complete_allnds; } /* process the directory */ result = process_dir(&dir, allnodes, cd, dir_list, basepathlen); /* close the directory */ isc_dir_close(&dir); if (result != ISC_R_SUCCESS) { goto complete_allnds; } /* get first dir entry from list. */ dir_entry = ISC_LIST_HEAD(*dir_list); while (dir_entry != NULL) { result = isc_dir_open(&dir, dir_entry->dirpath); /* if directory open failed, return error. */ if (result != ISC_R_SUCCESS) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Unable to open %s " "directory to read entries.", basepath); result = ISC_R_FAILURE; goto complete_allnds; } /* process the directory */ result = process_dir(&dir, allnodes, cd, dir_list, basepathlen); /* close the directory */ isc_dir_close(&dir); if (result != ISC_R_SUCCESS) { goto complete_allnds; } dir_entry = ISC_LIST_NEXT(dir_entry, link); } /* end while */ complete_allnds: /* clean up entries from list. */ dir_entry = ISC_LIST_HEAD(*dir_list); while (dir_entry != NULL) { next_de = ISC_LIST_NEXT(dir_entry, link); isc_mem_put(named_g_mctx, dir_entry, sizeof(dir_entry_t)); dir_entry = next_de; } /* end while */ isc_mem_put(named_g_mctx, dir_list, sizeof(dlist_t)); if (basepath != NULL) { isc_mem_free(named_g_mctx, basepath); } return (result); } static isc_result_t fs_findzone(void *driverarg, void *dbdata, const char *name, dns_clientinfomethods_t *methods, dns_clientinfo_t *clientinfo) { isc_result_t result; char *path; struct stat sb; path = NULL; UNUSED(driverarg); UNUSED(methods); UNUSED(clientinfo); if (create_path(name, NULL, NULL, (config_data_t *)dbdata, &path) != ISC_R_SUCCESS) { return (ISC_R_NOTFOUND); } isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_DEBUG(1), "Filesystem driver Findzone() Checking for path: '%s'\n", path); if (stat(path, &sb) != 0) { result = ISC_R_NOTFOUND; goto complete_FZ; } if ((sb.st_mode & S_IFDIR) != 0) { result = ISC_R_SUCCESS; goto complete_FZ; } result = ISC_R_NOTFOUND; complete_FZ: isc_mem_free(named_g_mctx, path); return (result); } static isc_result_t fs_lookup(const char *zone, const char *name, void *driverarg, void *dbdata, dns_sdlzlookup_t *lookup, dns_clientinfomethods_t *methods, dns_clientinfo_t *clientinfo) { isc_result_t result; char *path; struct stat sb; isc_dir_t dir; path = NULL; UNUSED(driverarg); UNUSED(lookup); UNUSED(methods); UNUSED(clientinfo); if (strcmp(name, "*") == 0) { /* * handle filesystem's special wildcard "-" */ result = create_path(zone, "-", NULL, (config_data_t *)dbdata, &path); } else { result = create_path(zone, name, NULL, (config_data_t *)dbdata, &path); } if (result != ISC_R_SUCCESS) { return (ISC_R_NOTFOUND); } /* remove path separator at end of path so stat works properly */ path[strlen(path) - 1] = '\0'; isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_DEBUG(1), "Filesystem driver lookup() Checking for path: '%s'\n", path); if (stat(path, &sb) != 0) { result = ISC_R_NOTFOUND; goto complete_lkup; } if ((sb.st_mode & S_IFDIR) == 0) { result = ISC_R_NOTFOUND; goto complete_lkup; } /* initialize and open directory */ isc_dir_init(&dir); result = isc_dir_open(&dir, path); /* if directory open failed, return error. */ if (result != ISC_R_SUCCESS) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Unable to open %s directory to read entries.", path); result = ISC_R_FAILURE; goto complete_lkup; } /* process any records in the directory */ result = process_dir(&dir, lookup, (config_data_t *)dbdata, NULL, 0); /* close the directory */ isc_dir_close(&dir); complete_lkup: isc_mem_free(named_g_mctx, path); return (result); } static isc_result_t fs_create(const char *dlzname, unsigned int argc, char *argv[], void *driverarg, void **dbdata) { config_data_t *cd; char *endp; int len; char pathsep; UNUSED(driverarg); UNUSED(dlzname); /* we require 5 command line args. */ if (argc != 6) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Filesystem driver requires " "6 command line args."); return (ISC_R_FAILURE); } if (strlen(argv[5]) > 1) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Filesystem driver can only " "accept a single character for separator."); return (ISC_R_FAILURE); } /* verify base dir ends with '/' or '\' */ len = strlen(argv[1]); if (argv[1][len - 1] != '\\' && argv[1][len - 1] != '/') { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Base dir parameter for filesystem driver " "should end with %s", "either '/' or '\\' "); return (ISC_R_FAILURE); } /* determine and save path separator for later */ if (argv[1][len - 1] == '\\') { pathsep = '\\'; } else { pathsep = '/'; } /* allocate memory for our config data */ cd = isc_mem_get(named_g_mctx, sizeof(config_data_t)); /* zero the memory */ memset(cd, 0, sizeof(config_data_t)); cd->pathsep = pathsep; /* get and store our base directory */ cd->basedir = isc_mem_strdup(named_g_mctx, argv[1]); cd->basedirsize = strlen(cd->basedir); /* get and store our data sub-dir */ cd->datadir = isc_mem_strdup(named_g_mctx, argv[2]); cd->datadirsize = strlen(cd->datadir); /* get and store our zone xfr sub-dir */ cd->xfrdir = isc_mem_strdup(named_g_mctx, argv[3]); cd->xfrdirsize = strlen(cd->xfrdir); /* get and store our directory split count */ cd->splitcnt = strtol(argv[4], &endp, 10); if (*endp != '\0' || cd->splitcnt < 0) { isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_ERROR, "Directory split count must be zero (0) " "or a positive number"); } /* get and store our separator character */ cd->separator = *argv[5]; /* attach config data to memory context */ isc_mem_attach(named_g_mctx, &cd->mctx); /* pass back config data */ *dbdata = cd; /* return success */ return (ISC_R_SUCCESS); } static void fs_destroy(void *driverarg, void *dbdata) { isc_mem_t *mctx; config_data_t *cd; UNUSED(driverarg); cd = (config_data_t *)dbdata; /* * free memory for each section of config data that was * allocated */ if (cd->basedir != NULL) { isc_mem_free(named_g_mctx, cd->basedir); } if (cd->datadir != NULL) { isc_mem_free(named_g_mctx, cd->datadir); } if (cd->xfrdir != NULL) { isc_mem_free(named_g_mctx, cd->xfrdir); } /* hold memory context to use later */ mctx = cd->mctx; /* free config data memory */ isc_mem_putanddetach(&mctx, cd, sizeof(config_data_t)); } static dns_sdlzmethods_t dlz_fs_methods = { fs_create, fs_destroy, fs_findzone, fs_lookup, NULL, fs_allnodes, fs_allowzonexfr, NULL, NULL, NULL, NULL, NULL, NULL, NULL, }; /*% * Wrapper around dns_sdlzregister(). */ isc_result_t dlz_fs_init(void) { isc_result_t result; /* * Write debugging message to log */ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_DEBUG(2), "Registering DLZ filesystem driver."); result = dns_sdlzregister("filesystem", &dlz_fs_methods, NULL, DNS_SDLZFLAG_RELATIVEOWNER | DNS_SDLZFLAG_RELATIVERDATA, named_g_mctx, &dlz_fs); if (result != ISC_R_SUCCESS) { UNEXPECTED_ERROR(__FILE__, __LINE__, "dns_sdlzregister() failed: %s", isc_result_totext(result)); result = ISC_R_UNEXPECTED; } return (result); } /*% * Wrapper around dns_sdlzunregister(). */ void dlz_fs_clear(void) { /* * Write debugging message to log */ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_DLZ, ISC_LOG_DEBUG(2), "Unregistering DLZ filesystem driver."); if (dlz_fs != NULL) { dns_sdlzunregister(&dlz_fs); } } #endif /* ifdef DLZ_FILESYSTEM */