|
|
/*++
Copyright (c) 1997-2000 Microsoft Corporation
Module Name:
cache.c
Abstract:
DNS Resolver Service
Cache routines
Author:
Glenn Curtis (glennc) March 1997
Revision History:
Jim Gilroy (jamesg) February 2000 cleanup
--*/
#include "local.h"
//
// Global Declarations
//
#define IS_STATIC_TTL_RECORD(prr) IS_HOSTS_FILE_RR(prr)
//
// Cache hash table
//
extern PCACHE_ENTRY * g_HashTable; extern DWORD g_CacheEntryCount; extern DWORD g_CacheRecordSetCount; extern DWORD g_CurrentCacheTime;
#define INITIAL_CACHE_HEAP_SIZE (16*1024)
//
// Compute a hash table index value for a string
//
#define EOS (L'\0')
#define COMPUTE_STRING_HASH_1( _String, _ulHashTableSize, _lpulHash ) \
{ \ PWCHAR p; \ ULOND h = 0, g; \ \ for ( p = _String; *p != EOS; p = p + 1 ) \ { \ h = ( h << 4 ) + (DWORD) (*p); \ if ( g = h&0xf0000000 ) \ { \ h = h ^ ( g >> 24 ); \ h = h ^ g; \ } \ } \ *_lpulHash = h % _ulHashTableSize; \ }
//
// Compute a hash table index value for a string
// which is invairant to case
//
#define COMPUTE_STRING_HASH_2( _String, _ulHashTableSize, _lpulHash ) \
{ \ PWCHAR _p = _String; \ PWCHAR _ep = _p + wcslen( _String ); \ ULONG h = 0; \ \ while( _p < _ep ) \ { \ h <<= 1; \ h ^= *_p++; \ } \ \ *_lpulHash = h % _ulHashTableSize; \ }
//
// Private prototypes
//
BOOL IsInvalidNegativeCacheEntry( IN PDNS_RECORD );
PDNS_RECORD BuildDNSServerRecord( IN IP_ADDRESS Address );
PDNS_RECORD BuildLocalAddressRecords( IN PSTR Name );
BOOL IsLocalAddress( IN IP_ADDRESS Ip );
//
// From ncache.c
//
BOOL makeCannonicalCacheName( OUT PWCHAR pNameBuffer, IN DWORD BufferLength, IN PWSTR pName, IN DWORD NameLength OPTIONAL );
DWORD getHashIndex( IN PWSTR pName, IN DWORD NameLength OPTIONAL );
//
// Define to new routine
//
#define PrepareRecordSetForRpc(prr) Cache_RestoreRecordListForRpc(prr)
#define IsCacheTtlStillValid(prr) Cache_IsRecordTtlValid(prr)
#define GetCacheLock() LOCK_CACHE()
#define ReleaseCacheLock() UNLOCK_CACHE()
//
// Cache entry routines
//
PCACHE_ENTRY CreateCacheEntry( IN PWSTR pName, IN BOOL fLoadingHostsFile ) /*++
Routine Description:
Create cache entry, including allocation.
Arguments:
pName -- name to create entry for
fLoadingHostsFile -- TRUE if loading from host file; FALSE otherwise
Return Value:
Ptr to newly allocated cache entry. NULL on error.
--*/ { ULONG index = 0; PCACHE_ENTRY pentry = NULL; DWORD nameLength; PWCHAR pnameCache = NULL;
DNSDBG( TRACE, ( "CreateCacheEntry( %S, hostfile=%d )\n", pName, fLoadingHostsFile ));
if ( !pName || !g_HashTable ) { return NULL; }
//
// build cache entry
//
// DCR_PERF: alloc for name within entry?
//
pentry = (PCACHE_ENTRY) CACHE_HEAP_ALLOC_ZERO( sizeof(CACHE_ENTRY) ); if ( !pentry ) { goto Fail; }
//
// build the name
//
nameLength = wcslen( pName );
pnameCache = CACHE_HEAP_ALLOC_ZERO( sizeof(WCHAR) * (nameLength + 1) ); if ( !pnameCache ) { goto Fail; }
if ( !makeCannonicalCacheName( pnameCache, nameLength+1, pName, nameLength ) ) { goto Fail; }
pentry->pName = pnameCache;
pentry->fHostsFileEntry = fLoadingHostsFile;
//
// insert cache entry into cache -- first entry in bucket
//
index = getHashIndex( pnameCache, 0 ); pentry->pNext = g_HashTable[ index ]; g_HashTable[ index ] = pentry; g_CacheEntryCount++;
if ( fLoadingHostsFile ) { ResizeCacheBucket( index, &g_CacheHashTableBucketSize ); } else { //
// Trim the hash bucket to keep the number of entries below
// g_CacheHashTableBucketSize.
//
// DCR: goofy cache limit
// DCR: cache limit should be total size above hosts file
// not individual buckets
// monitor thread should just wake up and and clear dead
// wood from cache
//
TrimCacheBucket( index, g_CacheHashTableBucketSize, TRUE ); }
return pentry;
Fail:
// dump entry
if ( pentry ) { CACHE_HEAP_FREE( pentry ); } if ( pnameCache ) { CACHE_HEAP_FREE( pnameCache ); } return NULL; }
//raw
VOID FreeCacheEntry( IN OUT PCACHE_ENTRY pEntry ) /*++
Routine Description:
Free cache entry.
Arguments:
pEntry -- cache entry to free
Globals:
g_CacheEntryCount -- decremented appropriately
g_NumberOfRecordsInCache -- decremented appropriately
Return Value:
None
--*/ { register INT iter;
DNSDBG( TRACE, ( "FreeCacheEntry( %p )\n", pEntry ));
//
// free entry
// - name
// - records
// - entry itself
//
// QUESTION: are global counters safe? should lock?
//
if ( pEntry ) { for ( iter = 0; iter < BASE_NUMBER_OF_RECORDS; iter++ ) { if ( pEntry->Records[iter] ) { Dns_RecordListFree( pEntry->Records[iter] ); g_CacheRecordSetCount--; } }
if ( pEntry->pName ) { CACHE_HEAP_FREE( pEntry->pName ); }
#if 0
if ( pEntry->pNext ) { DNSLOG_F1( "FreeCacheEntry is deleting an entry that still points to other entries!" ); } #endif
CACHE_HEAP_FREE( pEntry ); g_CacheEntryCount--; } }
//raw
VOID AddRecordToCacheEntry( IN OUT PCACHE_ENTRY pEntry, IN PDNS_RECORD pRecord, IN BOOL fFlushExisting, IN BOOL fLoadingHostsFile ) { WORD iter; WORD type = pRecord->wType;
DNSDBG( TRACE, ( "AddRecordToCacheEntry( e=%p, rr=%p, type=%d )\n", pEntry, pRecord, type ));
//
// don't overwrite host file entries
//
if ( !fLoadingHostsFile && pEntry->fHostsFileEntry && ( type == DNS_TYPE_A || type == DNS_TYPE_PTR ) ) { return; }
//
// clean up existing records at node
// - remove stale records
// - remove records of same type UNLESS not flushing existing
//
for ( iter = 0; iter < BASE_NUMBER_OF_RECORDS; iter++ ) { PDNS_RECORD prrExistSet = pEntry->Records[iter];
//
// skip sets of different type that are still valid
//
if ( !prrExistSet || ( prrExistSet->wType != type && IsCacheTtlStillValid( prrExistSet ) ) ) { continue; }
//
// dump
// - stale records
// - records of same type (when flush existing)
//
// note, that previous test means we're here with same
// type record OR record is invalid
//
if ( fFlushExisting || pEntry->Records[iter]->wType != type ) { Dns_RecordListFree( prrExistSet ); pEntry->Records[iter] = NULL; g_CacheRecordSetCount--; continue; }
//
// NOT flushing AND matching type -- host file load case
// => add new record to list
//
// the way this works
// - start at "record" which is addr of record ptr entry
// making pNext field the actual pointer
// - delete duplicates
// - tack new RR on end
// - blow away new RR name if existing record
//
//
// DCR: should have simple "make cache RR set" function that
// handles name and TTL issues
//
// DCR: broken if non-flush load hits wire data; wire data
// may have multiple RR sets
//
else { PDNS_RECORD prr; PDNS_RECORD prrPrev = (PDNS_RECORD) &pEntry->Records[iter];
while ( prr = prrPrev->pNext ) { // matches existing record?
// - cut existing record from list and free
if ( Dns_RecordCompare( prr, pRecord ) ) { prrPrev->pNext = prr->pNext; prr->pNext = NULL; Dns_RecordListFree( prr ); } else { prrPrev = prr; } }
//
// tack entry on to end
// - if existing records of type delete name
//
if ( prrPrev != (PDNS_RECORD)&pEntry->Records[iter] ) { if ( IS_FREE_OWNER(pRecord) ) { RECORD_HEAP_FREE( pRecord->pName ); pRecord->pName = NULL; } } prrPrev->pNext = pRecord; return; } }
//
// put record into cache entry
// - note record list may now contain pre-existing records
// from above
//
for ( iter = 0; iter < BASE_NUMBER_OF_RECORDS; iter++ ) { if ( pEntry->Records[iter] == NULL ) { pEntry->Records[iter] = pRecord; g_CacheRecordSetCount++; return; } }
//
// must find free spot in list -- get rid of last one
//
// DCR: realloc and push out if need more space
//
Dns_RecordListFree( pEntry->Records[BASE_NUMBER_OF_RECORDS-1] ); pEntry->Records[BASE_NUMBER_OF_RECORDS-1] = pRecord;
return; }
VOID FlushCacheEntry( IN PWSTR pName ) /*++
Routine Description:
Flush cache entry corresponding to a name.
Arguments:
pName -- name to delete from the cache
Return Value:
ERROR_SUCCESS if successful. ErrorCode on failure.
--*/ { ULONG index = 0; PCACHE_ENTRY pentry = NULL; PCACHE_ENTRY pprevEntry = NULL; WCHAR hashName[ DNS_MAX_NAME_BUFFER_LENGTH+4 ];
DNSDBG( TRACE, ( "FlushCacheEntry( %S )\n", pName )); if ( !g_HashTable ) { return; }
//
// build cache name
//
if ( !makeCannonicalCacheName( hashName, DNS_MAX_NAME_BUFFER_LENGTH, pName, 0 ) ) { return; }
//
// find entry in cache
//
// DCR: consider a find routine that lets us cut
// - return prev pointer
// - or know we're at front of list and have index
//
index = getHashIndex( hashName, 0 );
GetCacheLock();
pentry = g_HashTable[ index ];
while( pentry ) { if ( DnsNameCompare_W( hashName, pentry->pName ) ) { //
// Can't purge cache entries that hold records that were
// read in from the hosts file . . .
//
if ( pentry->fHostsFileEntry ) { goto Done; }
//
// Found it!
//
if ( pprevEntry ) { //
// There is an entry in front of the one we found
// in the list.
//
pprevEntry->pNext = pentry->pNext; } else { //
// There isn't an entry in front of the one we found.
//
g_HashTable[ index ] = pentry->pNext; }
pentry->pNext = NULL; FreeCacheEntry( pentry ); goto Done; }
pprevEntry = pentry; pentry = pentry->pNext; }
Done:
ReleaseCacheLock(); }
VOID FlushCacheEntryRecord( IN PWSTR pName, IN WORD Type ) /*++
Routine Description:
Flush cached records corresponding to a name and type.
Arguments:
pName -- name of records to delete
Type -- type of records to delete
Return Value:
ERROR_SUCCESS if successful. ErrorCode on failure.
--*/ { WORD iter; PCACHE_ENTRY pentry;
DNSDBG( TRACE, ( "FlushCacheEntryRecord( %S, %d )\n", pName, Type ));
//
// find entry in cache
//
GetCacheLock();
pentry = FindCacheEntry( pName ); if ( !pentry ) { goto Done; }
//
// free records for entry
// - not from hosts file
// - matches type
// - or type zero (for deleting ALL records)
// - or name error
//
for ( iter = 0; iter < BASE_NUMBER_OF_RECORDS; iter++ ) { PDNS_RECORD prr = pentry->Records[iter];
if ( prr && ! IS_HOSTS_FILE_RR(prr) && ( prr->wType == Type || Type == 0 || ( prr->wType == DNS_TYPE_ANY && prr->wDataLength == 0 ) ) ) { Dns_RecordListFree( prr ); pentry->Records[iter] = NULL; g_CacheRecordSetCount--; } }
Done:
ReleaseCacheLock(); }
//raw
VOID TrimCache( VOID ) /*++
Routine Description:
Trim back cache. Trim every bucket in cache.
Arguments:
None
Return Value:
None
--*/ { WORD hashIter;
if ( !g_HashTable ) return;
for ( hashIter = 0; hashIter < g_CacheHashTableSize; hashIter++ ) { TrimCacheBucket( hashIter, g_CacheHashTableBucketSize, FALSE ); } }
//raw
VOID TrimCacheBucket( IN ULONG Index, IN DWORD dwBucketSize, IN BOOL fSkipFirst ) /*++
Routine Description:
Trim back cache bucket.
Arguments:
Index -- Index of hash bucket to trim.
dwBucketSize -- size to trim bucket to
fSkipFirst -- skip first entry while triming
Return Value:
None
--*/ { PCACHE_ENTRY pentry = g_HashTable[ Index ]; PCACHE_ENTRY pprevEntry = NULL; DWORD EntryCount = 0;
//
// skip first entry in bucket
//
if ( pentry && fSkipFirst ) { pprevEntry = pentry; pentry = pentry->pNext; EntryCount++; }
while ( pentry ) { PCACHE_ENTRY pnextEntry = pentry->pNext; WORD recordCount = 0; WORD iter;
for ( iter = 0; iter < BASE_NUMBER_OF_RECORDS; iter++ ) { //
// delete stale entries
// - not from host file
// - TTL expired
//
if ( ! pentry->fHostsFileEntry && pentry->Records[iter] && !IsCacheTtlStillValid( pentry->Records[iter] ) ) { Dns_RecordListFree( pentry->Records[iter] ); pentry->Records[iter] = NULL; g_CacheRecordSetCount--; recordCount++; } else if ( !pentry->Records[iter] ) { recordCount++; } }
//
// if entry is empty -- delete
//
if ( recordCount == BASE_NUMBER_OF_RECORDS ) { if ( pprevEntry ) pprevEntry->pNext = pentry->pNext; else g_HashTable[ Index ] = pentry->pNext;
pentry->pNext = NULL;
FreeCacheEntry( pentry ); } else { pprevEntry = pentry; EntryCount++; }
pentry = pnextEntry; }
//
// if still too many entries we need to delete
//
// DCR: cache size limitation is weak
// - ideally time out based on query, for instances pick an interval
// say ten minutes and time out all older stuff
// - or better yet do LRU timeout
//
if ( EntryCount >= dwBucketSize ) { PCACHE_ENTRY pPrevPurgeEntry = NULL; PCACHE_ENTRY pPurgeEntry = NULL;
pentry = g_HashTable[ Index ]; pprevEntry = NULL;
//
// skip first when required
//
if ( pentry && fSkipFirst ) { pprevEntry = pentry; pentry = pentry->pNext; }
//
// Loop through all cache entries looking for potential ones
// to get rid of, the last one in the list will be the one that
// is purged (Least Recently Used).
//
while ( pentry ) { if ( ! pentry->fHostsFileEntry ) { //
// Found a potential entry to purge
//
pPrevPurgeEntry = pprevEntry; pPurgeEntry = pentry; }
pprevEntry = pentry; pentry = pentry->pNext; }
//
// Now get rid of the entry that was found
//
// FIXME: how many times are we going to clone this code???
//
if ( pPurgeEntry ) { WORD iter;
if ( pPrevPurgeEntry ) { pPrevPurgeEntry->pNext = pPurgeEntry->pNext; } else { g_HashTable[ Index ] = pPurgeEntry->pNext; }
pPurgeEntry->pNext = NULL;
FreeCacheEntry( pPurgeEntry ); } } }
//raw
VOID ResizeCacheBucket( IN ULONG Index, IN PDWORD pdwBucketSize ) { PCACHE_ENTRY pentry = g_HashTable[ Index ]; DWORD Count = 0;
while ( pentry ) { Count++; pentry = pentry->pNext; }
if ( (*pdwBucketSize - Count) < 10 ) { *pdwBucketSize += 10; } }
//raw
PCACHE_ENTRY FindCacheEntry( IN PWSTR pName ) { ULONG index; PCACHE_ENTRY pentry; PCACHE_ENTRY pprevEntry = NULL; WCHAR hashName[ DNS_MAX_NAME_BUFFER_LENGTH+4 ];
if ( !g_HashTable ) { return NULL; } if ( !pName ) { DNS_ASSERT( FALSE ); return NULL; }
DNSDBG( TRACE, ( "FindCacheEntry( %S )\n", pName ));
//
// build cache name
// - if invalid (too long) bail
//
if ( !makeCannonicalCacheName( hashName, DNS_MAX_NAME_BUFFER_LENGTH, pName, 0 ) ) { return NULL; }
//
// find entry in cache
//
index = getHashIndex( hashName, 0 );
pentry = g_HashTable[ index ];
DNSDBG( TRACE, ( "in FindCacheEntry\n" "\tname = %S\n" "\tindex = %d\n" "\tpentry = %p\n", hashName, index, pentry ));
while( pentry ) { if ( DnsNameCompare_W( hashName, pentry->pName ) ) { //
// found entry
// - move to front, if not already there
if ( pprevEntry ) { pprevEntry->pNext = pentry->pNext; pentry->pNext = g_HashTable[ index ]; g_HashTable[ index ] = pentry; } break; } ELSE { DNSDBG( OFF, ( "in FindCacheEntry -- failed name compare\n" "\tout name = %S\n" "\tpentry = %p\n" "\tname = %S\n", hashName, pentry, pentry->pName )); }
pprevEntry = pentry; pentry = pentry->pNext; }
DNSDBG( TRACE, ( "Leave FindCacheEntry\n" "\tname = %S\n" "\tindex = %d\n" "\tpentry = %p\n", hashName, index, pentry ));
return pentry; }
PCACHE_ENTRY FindOrCreateCacheEntry( IN PWSTR pName, IN BOOL fHostsFile ) /*++
Routine Description:
Find or create entry for name in cache.
Arguments:
pName -- name to find
Return Value:
Ptr to cache entry -- if successful. NULL on failure.
--*/ { PCACHE_ENTRY pentry;
DNSDBG( TRACE, ( "FindOrCreateCacheEntry( %S, hosts=%d )\n", pName, fHostsFile ));
//
// find entry?
//
pentry = FindCacheEntry( pName ); if ( !pentry ) { pentry = CreateCacheEntry( pName, fHostsFile ); }
DNSDBG( TRACE, ( "Leave FindOrCreateCacheEntry( %S, hosts=%d ) => %p\n", pName, fHostsFile, pentry ));
return pentry; }
PDNS_RECORD FindCacheEntryRecord( IN PCACHE_ENTRY pEntry, IN WORD Type ) /*++
Routine Description:
Find entry in cache.
Arguments:
pEntry -- cache entry to check
Type -- record type to find
Return Value:
Ptr to record set of desired type -- if found. NULL if not found.
--*/ { WORD iter; PDNS_RECORD prr;
DNSDBG( TRACE, ( "FindCacheEntryRecord( %p, type=%d )\n", pEntry, Type ));
//
// check all the records at the cache entry
//
for ( iter = 0; iter < BASE_NUMBER_OF_RECORDS; iter++ ) { prr = pEntry->Records[iter];
if ( prr && ( prr->wType == Type || prr->wType == DNS_TYPE_CNAME || ( prr->wType == DNS_TYPE_ANY && prr->wDataLength == 0 ) ) ) { //
// if expired dump RR list
//
// DCR: if different RR sets in list, then TTL check here not sufficient
//
// DCR: functionalize this
//
if ( !IsCacheTtlStillValid( prr ) || IsInvalidNegativeCacheEntry( prr ) ) { DNSDBG( TRACE, ( "Whacking timed out record %p at cache entry %p\n", prr, pEntry )); Dns_RecordListFree( prr ); pEntry->Records[iter] = NULL; g_CacheRecordSetCount--; prr = NULL; goto Done; }
//
// If the cached record is a CNAME, walk the record
// list to see if the CNAME chain points to a record
// that is the type we are looking for.
//
if ( prr->wType == DNS_TYPE_CNAME && Type != DNS_TYPE_CNAME ) { PDNS_RECORD prrChain = prr->pNext;
while ( prrChain ) { if ( prrChain->wType == Type ) { // chain to desired type -- take RR set
goto Done; } prrChain = prrChain->pNext; } prr = NULL; goto Done; }
// take RR set
goto Done; } } // type not found
prr = NULL;
Done:
DNSDBG( TRACE, ( "Leave FindCacheEntryRecord() => %p\n", prr ));
return prr; }
PDNS_RECORD FindCacheRecords( IN PWSTR pwsName, IN WORD wType ) /*++
Routine Description:
Find records of given name and type in cache.
Arguments:
pwsName -- name
Type -- record type to find
Return Value:
Ptr to record set of desired type -- if found. NULL if not found.
--*/ { PCACHE_ENTRY pentry; PDNS_RECORD prr = NULL;
DNSDBG( TRACE, ( "FindCacheRecords( %S, type=%d )\n", pwsName, wType ));
pentry = FindCacheEntry( pwsName ); if ( pentry ) { prr = FindCacheEntryRecord( pentry, wType ); }
DNSDBG( TRACE, ( "Leave FindCacheRecords( %S, type=%d ) => %p\n", pwsName, wType, prr ));
return prr; }
//raw
VOID CacheAnyAdditionalRecords( IN OUT PDNS_RECORD pRecord, IN BOOL fHostFile ) { BOOL fcnameAnswer = FALSE; PCACHE_ENTRY pentry = NULL; PDNS_RECORD pnextRR = pRecord; PDNS_RECORD prr;
DNSDBG( TRACE, ( "CacheAnyAdditionalRecords( rr=%p, hosts=%d )\n", pRecord, fHostFile ));
//
// cache "additional" records
//
// this is really cache additional OR
// cache the answer records for a CNAME at that CNAME
//
// background: Glenn's caching paradigm was to cache all answer
// data at the queried name in the API call (name might be short).
// However, not caching the CNAME data can cause problems, so this
// was tacked on.
//
// For CNAME caching we throw away the CNAMEs themselves and just
// cache the actually data (address) records at the CNAME node.
//
//
// cache additional records
//
while ( prr = pnextRR ) { BOOL fcacheSet = FALSE;
pnextRR = Dns_RecordSetDetach( prr );
//
// do NOT cache
// - answer records for queried name (not CNAME)
// - CNAME records when doing caching of answer data under CNAME
// - authority section records (NS, SOA, etc)
// - OPT records
//
// DCR: have some sort of "cacheable type" test
// which would screen out any transactional records
//
if ( fHostFile ) { fcacheSet = TRUE; } else if ( prr->Flags.S.Section == DNSREC_ANSWER ) { if ( prr->wType == DNS_TYPE_CNAME ) { fcnameAnswer = TRUE; } else if ( fcnameAnswer ) { fcacheSet = TRUE; } } else if ( prr->Flags.S.Section == DNSREC_ADDITIONAL ) { if ( prr->wType != DNS_TYPE_OPT ) { fcacheSet = TRUE; } }
if ( !fcacheSet ) { Dns_RecordListFree( prr ); continue; }
//
// cache the set
//
// flip the section field to "Answer" section
//
// DCR: section caching?
//
// note: section fields in cache indicate whether
// answer data (or additional) once out of
// cache;
// this is necessary since we cache everything
// at node and return it in one RR list; we'd
// to change must
// - return in different lists with some indication
// in cache of what's what
// OR
// - another indication of what's what
//
pentry = FindOrCreateCacheEntry( prr->pName, fHostFile ); if ( !pentry ) { Dns_RecordListFree( prr ); } else { //if ( !fHostFile )
// currently HostFile entries get answer too
{ PDNS_RECORD ptemp = prr; while ( ptemp ) { ptemp->Flags.S.Section = DNSREC_ANSWER; ptemp = ptemp->pNext; } } PrepareRecordSetForCache( prr ); AddRecordToCacheEntry( pentry, prr, ! fHostFile, // flush if NOT hostfile load
fHostFile ); } }
DNSDBG( TRACE, ( "Leave CacheAnyAdditionalRecords()\n" )); }
//raw
BOOL IsInvalidNegativeCacheEntry( IN PDNS_RECORD pRecord ) { DWORD cacheTime;
if ( pRecord->wDataLength != 0 ) { return FALSE; }
//
// recover record cache time
//
cacheTime = g_MaxNegativeCacheTtl; if ( pRecord->wType == DNS_TYPE_SOA ) { cacheTime = g_NegativeSOACacheTime; }
// should NEVER have absolute time less than time we cached for
ASSERT( cacheTime <= pRecord->dwTtl );
//
// check if last PnP AFTER this record was cached
//
DNSDBG( TRACE, ( "IsInvalidNegativeCacheEntry( rr=%p ) => %d\n", pRecord, ( g_TimeOfLastPnPUpdate > (pRecord->dwTtl - cacheTime)) ));
return( g_TimeOfLastPnPUpdate > (pRecord->dwTtl - cacheTime) ); }
//
// End cache.c
//
|