/* Unix SMB/CIFS implementation. DNS tombstoning routines Copyright (C) Andrew Bartlett 2018 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "includes.h" #include #include "../lib/util/dlinklist.h" #include "librpc/gen_ndr/ndr_misc.h" #include "librpc/gen_ndr/ndr_drsuapi.h" #include "librpc/gen_ndr/ndr_drsblobs.h" #include "param/param.h" #include "lib/util/dlinklist.h" #include "ldb.h" #include "dsdb/kcc/scavenge_dns_records.h" #include "lib/ldb-samba/ldb_matching_rules.h" #include "lib/util/time.h" #include "dns_server/dnsserver_common.h" #include "librpc/gen_ndr/ndr_dnsp.h" #include "param/param.h" #include "librpc/gen_ndr/ndr_misc.h" #include "librpc/gen_ndr/ndr_drsuapi.h" #include "librpc/gen_ndr/ndr_drsblobs.h" /* * Copy only non-expired dns records from one message element to another. */ static NTSTATUS copy_current_records(TALLOC_CTX *mem_ctx, struct ldb_message_element *old_el, struct ldb_message_element *el, uint32_t dns_timestamp) { unsigned int i; struct dnsp_DnssrvRpcRecord rec; enum ndr_err_code ndr_err; el->values = talloc_zero_array(mem_ctx, struct ldb_val, old_el->num_values); if (el->values == NULL) { return NT_STATUS_NO_MEMORY; } for (i = 0; i < old_el->num_values; i++) { ndr_err = ndr_pull_struct_blob( &(old_el->values[i]), mem_ctx, &rec, (ndr_pull_flags_fn_t)ndr_pull_dnsp_DnssrvRpcRecord); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { DBG_ERR("Failed to pull dns rec blob.\n"); return NT_STATUS_INTERNAL_ERROR; } if (rec.dwTimeStamp > dns_timestamp || rec.dwTimeStamp == 0) { el->values[el->num_values] = old_el->values[i]; el->num_values++; } } return NT_STATUS_OK; } /* * Check all records in a zone and tombstone them if they're expired. */ static NTSTATUS dns_tombstone_records_zone(TALLOC_CTX *mem_ctx, struct ldb_context *samdb, struct dns_server_zone *zone, uint32_t dns_timestamp, NTTIME entombed_time, char **error_string) { WERROR werr; NTSTATUS status; unsigned int i; struct dnsserver_zoneinfo *zi = NULL; struct ldb_result *res = NULL; struct ldb_message_element *el = NULL; struct ldb_message_element *tombstone_el = NULL; struct ldb_message_element *old_el = NULL; struct ldb_message *new_msg = NULL; enum ndr_err_code ndr_err; int ret; struct GUID guid; struct GUID_txt_buf buf_guid; const char *attrs[] = {"dnsRecord", "dNSTombstoned", "objectGUID", NULL}; struct ldb_val true_val = { .data = discard_const_p(uint8_t, "TRUE"), .length = 4 }; struct ldb_val tombstone_blob; struct dnsp_DnssrvRpcRecord tombstone_struct = { .wType = DNS_TYPE_TOMBSTONE, .data = {.EntombedTime = entombed_time} }; ndr_err = ndr_push_struct_blob( &tombstone_blob, mem_ctx, &tombstone_struct, (ndr_push_flags_fn_t)ndr_push_dnsp_DnssrvRpcRecord); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { *error_string = discard_const_p(char, "Failed to push TOMBSTONE" "dnsp_DnssrvRpcRecord\n"); return NT_STATUS_INTERNAL_ERROR; } *error_string = NULL; /* Get NoRefreshInterval and RefreshInterval from zone properties.*/ zi = talloc(mem_ctx, struct dnsserver_zoneinfo); if (zi == NULL) { return NT_STATUS_NO_MEMORY; } werr = dns_get_zone_properties(samdb, mem_ctx, zone->dn, zi); if (W_ERROR_EQUAL(DNS_ERR(NOTZONE), werr)) { return NT_STATUS_PROPSET_NOT_FOUND; } else if (!W_ERROR_IS_OK(werr)) { return NT_STATUS_INTERNAL_ERROR; } /* Subtract them from current time to get the earliest possible. * timestamp allowed for a non-expired DNS record. */ dns_timestamp -= zi->dwNoRefreshInterval + zi->dwRefreshInterval; /* Custom match gets dns records in the zone with dwTimeStamp < t. */ ret = ldb_search(samdb, mem_ctx, &res, zone->dn, LDB_SCOPE_SUBTREE, attrs, "(&(objectClass=dnsNode)" "(&(!(dnsTombstoned=TRUE))" "(dnsRecord:" DSDB_MATCH_FOR_DNS_TO_TOMBSTONE_TIME ":=%"PRIu32")))", dns_timestamp); if (ret != LDB_SUCCESS) { *error_string = talloc_asprintf(mem_ctx, "Failed to search for dns " "objects in zone %s: %s", ldb_dn_get_linearized(zone->dn), ldb_errstring(samdb)); return NT_STATUS_INTERNAL_ERROR; } /* * Do a constrained update on each expired DNS node. To do a constrained * update we leave the dnsRecord element as is, and just change the flag * to MOD_DELETE, then add a new element with the changes we want. LDB * will run the deletion first, and bail out if a binary comparison * between the attribute we pass and the one in the database shows a * change. This prevents race conditions. */ for (i = 0; i < res->count; i++) { new_msg = ldb_msg_copy(mem_ctx, res->msgs[i]); if (new_msg == NULL) { return NT_STATUS_INTERNAL_ERROR; } old_el = ldb_msg_find_element(new_msg, "dnsRecord"); if (old_el == NULL) { TALLOC_FREE(new_msg); return NT_STATUS_INTERNAL_ERROR; } old_el->flags = LDB_FLAG_MOD_DELETE; ret = ldb_msg_add_empty( new_msg, "dnsRecord", LDB_FLAG_MOD_ADD, &el); if (ret != LDB_SUCCESS) { TALLOC_FREE(new_msg); return NT_STATUS_INTERNAL_ERROR; } status = copy_current_records(new_msg, old_el, el, dns_timestamp); if (!NT_STATUS_IS_OK(status)) { TALLOC_FREE(new_msg); return NT_STATUS_INTERNAL_ERROR; } /* If nothing was expired, do nothing. */ if (el->num_values == old_el->num_values && el->num_values != 0) { TALLOC_FREE(new_msg); continue; } /* * If everything was expired, we tombstone the node, which * involves adding a tombstone dnsRecord and a 'dnsTombstoned: * TRUE' attribute. That is, we want to end up with this: * * objectClass: dnsNode * dnsRecord: { .wType = DNSTYPE_TOMBSTONE, * .data.EntombedTime = } * dnsTombstoned: TRUE * * and no other dnsRecords. */ if (el->num_values == 0) { struct ldb_val *vals = talloc_realloc(new_msg->elements, el->values, struct ldb_val, 1); if (!vals) { TALLOC_FREE(new_msg); return NT_STATUS_INTERNAL_ERROR; } el->values = vals; el->values[0] = tombstone_blob; el->num_values = 1; tombstone_el = ldb_msg_find_element(new_msg, "dnsTombstoned"); if (tombstone_el == NULL) { ret = ldb_msg_add_value(new_msg, "dnsTombstoned", &true_val, &tombstone_el); if (ret != LDB_SUCCESS) { TALLOC_FREE(new_msg); return NT_STATUS_INTERNAL_ERROR; } tombstone_el->flags = LDB_FLAG_MOD_ADD; } else { if (tombstone_el->num_values != 1) { vals = talloc_realloc( new_msg->elements, tombstone_el->values, struct ldb_val, 1); if (!vals) { TALLOC_FREE(new_msg); return NT_STATUS_INTERNAL_ERROR; } tombstone_el->values = vals; tombstone_el->num_values = 1; } tombstone_el->flags = LDB_FLAG_MOD_REPLACE; tombstone_el->values[0] = true_val; } } else { /* * Do not change the status of dnsTombstoned if we * found any live records. If it exists, its value * will be the harmless "FALSE", which is what we end * up with when a tombstoned record is untombstoned. * (in dns_common_replace). */ ldb_msg_remove_attr(new_msg, "dnsTombstoned"); } /* Set DN to the GUID in case the object was moved. */ el = ldb_msg_find_element(new_msg, "objectGUID"); if (el == NULL) { TALLOC_FREE(new_msg); *error_string = talloc_asprintf(mem_ctx, "record has no objectGUID " "in zone %s", ldb_dn_get_linearized(zone->dn)); return NT_STATUS_INTERNAL_ERROR; } status = GUID_from_ndr_blob(el->values, &guid); if (!NT_STATUS_IS_OK(status)) { TALLOC_FREE(new_msg); *error_string = discard_const_p(char, "Error: Invalid GUID.\n"); return NT_STATUS_INTERNAL_ERROR; } GUID_buf_string(&guid, &buf_guid); new_msg->dn = ldb_dn_new_fmt(mem_ctx, samdb, "", buf_guid.buf); /* Remove the GUID so we're not trying to modify it. */ ldb_msg_remove_attr(new_msg, "objectGUID"); ret = ldb_modify(samdb, new_msg); if (ret != LDB_SUCCESS) { TALLOC_FREE(new_msg); *error_string = talloc_asprintf(mem_ctx, "Failed to modify dns record " "in zone %s: %s", ldb_dn_get_linearized(zone->dn), ldb_errstring(samdb)); return NT_STATUS_INTERNAL_ERROR; } TALLOC_FREE(new_msg); } return NT_STATUS_OK; } /* * Tombstone all expired DNS records. */ NTSTATUS dns_tombstone_records(TALLOC_CTX *mem_ctx, struct ldb_context *samdb, char **error_string) { struct dns_server_zone *zones = NULL; struct dns_server_zone *z = NULL; NTSTATUS ret; uint32_t dns_timestamp; NTTIME entombed_time; TALLOC_CTX *tmp_ctx = NULL; time_t unix_now = time(NULL); unix_to_nt_time(&entombed_time, unix_now); dns_timestamp = unix_to_dns_timestamp(unix_now); tmp_ctx = talloc_new(mem_ctx); if (tmp_ctx == NULL) { return NT_STATUS_NO_MEMORY; } ret = dns_common_zones(samdb, tmp_ctx, NULL, &zones); if (!NT_STATUS_IS_OK(ret)) { TALLOC_FREE(tmp_ctx); return ret; } for (z = zones; z; z = z->next) { ret = dns_tombstone_records_zone(tmp_ctx, samdb, z, dns_timestamp, entombed_time, error_string); if (NT_STATUS_EQUAL(ret, NT_STATUS_PROPSET_NOT_FOUND)) { continue; } else if (!NT_STATUS_IS_OK(ret)) { TALLOC_FREE(tmp_ctx); return ret; } } TALLOC_FREE(tmp_ctx); return NT_STATUS_OK; } /* * Delete all DNS tombstones that have been around for longer than the server * property 'dns_tombstone_interval' which we store in smb.conf, which * corresponds to DsTombstoneInterval in [MS-DNSP] 3.1.1.1.1 "DNS Server * Integer Properties". */ NTSTATUS dns_delete_tombstones(TALLOC_CTX *mem_ctx, struct ldb_context *samdb, char **error_string) { struct dns_server_zone *zones = NULL; struct dns_server_zone *z = NULL; int ret, i; NTSTATUS status; uint32_t current_time; uint32_t tombstone_interval; uint32_t tombstone_hours; NTTIME tombstone_nttime; enum ndr_err_code ndr_err; struct ldb_result *res = NULL; TALLOC_CTX *tmp_ctx = NULL; struct loadparm_context *lp_ctx = NULL; struct ldb_message_element *el = NULL; struct dnsp_DnssrvRpcRecord rec = {0}; const char *attrs[] = {"dnsRecord", "dNSTombstoned", NULL}; current_time = unix_to_dns_timestamp(time(NULL)); lp_ctx = (struct loadparm_context *)ldb_get_opaque(samdb, "loadparm"); tombstone_interval = lpcfg_parm_ulong(lp_ctx, NULL, "dnsserver", "dns_tombstone_interval", 24 * 14); tombstone_hours = current_time - tombstone_interval; status = dns_timestamp_to_nt_time(&tombstone_nttime, tombstone_hours); if (!NT_STATUS_IS_OK(status)) { DBG_ERR("DNS timestamp exceeds NTTIME epoch.\n"); return NT_STATUS_INTERNAL_ERROR; } tmp_ctx = talloc_new(mem_ctx); if (tmp_ctx == NULL) { return NT_STATUS_NO_MEMORY; } status = dns_common_zones(samdb, tmp_ctx, NULL, &zones); if (!NT_STATUS_IS_OK(status)) { TALLOC_FREE(tmp_ctx); return status; } for (z = zones; z; z = z->next) { /* * This can load a very large set, but on the * assumption that the number of tombstones is * relatively small compared with the number of active * records, and that this is an indexed lookup, this * should be OK. We can make a match rule if * returning the set of tombstones becomes an issue. */ ret = ldb_search(samdb, tmp_ctx, &res, z->dn, LDB_SCOPE_SUBTREE, attrs, "(&(objectClass=dnsNode)(dNSTombstoned=TRUE))"); if (ret != LDB_SUCCESS) { *error_string = talloc_asprintf(mem_ctx, "Failed to " "search for tombstoned " "dns objects in zone %s: %s", ldb_dn_get_linearized(z->dn), ldb_errstring(samdb)); TALLOC_FREE(tmp_ctx); return NT_STATUS_INTERNAL_ERROR; } for (i = 0; i < res->count; i++) { struct ldb_message *msg = res->msgs[i]; el = ldb_msg_find_element(msg, "dnsRecord"); if (el == NULL) { DBG_ERR("The tombstoned dns node %s has no dns " "records, which should not happen.\n", ldb_dn_get_linearized(msg->dn) ); continue; } /* * Below we assume the element has one value, which we * expect because when we tombstone a node we remove * all the records except for the tombstone. */ if (el->num_values != 1) { DBG_ERR("The tombstoned dns node %s has %u " "dns records, expected one.\n", ldb_dn_get_linearized(msg->dn), el->num_values ); continue; } ndr_err = ndr_pull_struct_blob( el->values, tmp_ctx, &rec, (ndr_pull_flags_fn_t)ndr_pull_dnsp_DnssrvRpcRecord); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { TALLOC_FREE(tmp_ctx); DBG_ERR("Failed to pull dns rec blob.\n"); return NT_STATUS_INTERNAL_ERROR; } if (rec.wType != DNS_TYPE_TOMBSTONE) { DBG_ERR("A tombstoned dnsNode has non-tombstoned" " records, which should not happen.\n"); continue; } if (rec.data.EntombedTime > tombstone_nttime) { continue; } /* * Between 4.9 and 4.14 in some places we saved the * tombstone time as hours since the start of 1601, * not in NTTIME ten-millionths of a second units. * * We can accommodate these bad values by noting that * all the realistic timestamps in that measurement * fall within the first *second* of NTTIME, that is, * before 1601-01-01 00:00:01; and that these * timestamps are not realistic for NTTIME timestamps. * * Calculation: there are roughly 365.25 * 24 = 8766 * hours per year, and < 500 years since 1601, so * 4383000 would be a fine threshold. We round up to * the crore-second (c. 2741CE) in honour of NTTIME. */ if ((rec.data.EntombedTime < 10000000) && (rec.data.EntombedTime > tombstone_hours)) { continue; } ret = dsdb_delete(samdb, msg->dn, 0); if (ret != LDB_ERR_NO_SUCH_OBJECT && ret != LDB_SUCCESS) { TALLOC_FREE(tmp_ctx); DBG_ERR("Failed to delete dns node \n"); return NT_STATUS_INTERNAL_ERROR; } } } TALLOC_FREE(tmp_ctx); return NT_STATUS_OK; }