import assert from "assert" ; import deepEqual from "fast-deep-equal" ; import { dnsLowerCase } from "../../util/dns-equal" ; import { DNSLabelCoder } from "../DNSLabelCoder" ; import { DecodedData , RType } from "../DNSPacket" ; import { RecordRepresentation , ResourceRecord } from "../ResourceRecord" ; interface RRTypeWindow { windowId : number ; bitMapSize : number ; rrtypes : RType [ ] ; } export class NSECRecord extends ResourceRecord { readonly nextDomainName : string ; private lowerCasedNextDomainName ? : string ; readonly rrTypeWindows : RRTypeWindow [ ] ; constructor ( name , nextDomainName , rrtypes , ttl , flushFlag ? ) ; constructor ( header , nextDomainName , rrtypes ) ; constructor ( name , nextDomainName , rrtypes , ttl ? , flushFlag ? ) { if ( typeof name === "string" ) { super ( name , RType . NSEC , ttl || NSECRecord . RR_DEFAULT_TTL_SHORT , flushFlag ) ; } else { assert ( name . type === RType . NSEC ) ; super ( name ) ; } if ( ! nextDomainName . endsWith ( "." ) ) { nextDomainName += "." ; } this . nextDomainName = nextDomainName ; this . rrTypeWindows = NSECRecord . rrTypesToWindowMap ( rrtypes ) ; } public getLowerCasedNextDomainName ( ) { return this . lowerCasedNextDomainName || ( this . lowerCasedNextDomainName = dnsLowerCase ( this . nextDomainName ) ) ; } private getRRTypesBitMapEncodingLength ( ) { let rrTypesBitMapLength = 0 ; for ( const window of this . rrTypeWindows ) { assert ( window . rrtypes . length > 0 , "types array for windowId " + window . windowId + " cannot be empty!" ) ; rrTypesBitMapLength += 2 + window . bitMapSize ; } return rrTypesBitMapLength ; } protected getRDataEncodingLength ( coder ) { return ( coder . legacyUnicastEncoding ? coder . getUncompressedNameLength ( this . nextDomainName ) : coder . getUncompressedNameLength ( this . nextDomainName ) ) + this . getRRTypesBitMapEncodingLength ( ) ; } protected encodeRData ( coder , buffer , offset ) { const oldOffset = offset ; const length = coder . legacyUnicastEncoding ? coder . encodeUncompressedName ( this . nextDomainName , offset ) : coder . encodeUncompressedName ( this . nextDomainName , offset ) ; offset += length ; for ( const window of this . rrTypeWindows ) { buffer . writeUInt8 ( window . windowId , offset ++ ) ; buffer . writeUInt8 ( window . bitMapSize , offset ++ ) ; const bitmap = Buffer . alloc ( window . bitMapSize ) ; for ( const type of window . rrtypes ) { const byteNum = ( type & 0xFF ) >> 3 ; let mask = bitmap . readUInt8 ( byteNum ) ; mask |= 1 << ( 7 - ( type & 0x7 ) ) ; bitmap . writeUInt8 ( mask , byteNum ) ; } bitmap . copy ( buffer , offset ) ; offset += bitmap . length ; } return offset - oldOffset ; } public static decodeData ( coder , header , buffer , offset ) { const oldOffset = offset ; /**
         * Quick note to the line below. We base "false" as the second argument to decodeName, telling
         * it to not resolve pointers.
         * We discovered that especially UniFi routers with a VLAN setup and mdns forwarding enabled,
         * fail to properly encode pointers inside the nextDomainName field.
         * Those pointers simply point to random points in the record data, resulting in decoding to fail.
         * As the field doesn't have any meaning and we simply don't use it, we just skip decoding for now.
         */ const decodedNextDomainName = coder . decodeName ( offset , false ) ; offset += decodedNextDomainName . readBytes ; const rrTypes = [ ] ; while ( offset < buffer . length ) { const windowId = buffer . readUInt8 ( offset ++ ) ; const bitMapLength = buffer . readUInt8 ( offset ++ ) ; const upperRType = windowId << 8 ; for ( let block = 0 ; block < bitMapLength ; block ++ ) { const byte = buffer . readUInt8 ( offset ++ ) ; for ( let bit = 0 ; bit < 8 ; bit ++ ) { if ( byte & ( 1 << ( 7 - bit ) ) ) { const rType = upperRType | ( block << 3 ) | bit ; rrTypes . push ( rType ) ; } } } } return { data : new NSECRecord ( header , decodedNextDomainName . data , rrTypes ) , readBytes : offset - oldOffset , } ; } public clone ( ) { return new NSECRecord ( this . getRecordRepresentation ( ) , this . nextDomainName , NSECRecord . windowsToRRTypes ( this . rrTypeWindows ) ) ; } public dataAsString ( ) { return `${ this . nextDomainName } [${ NSECRecord . windowsToRRTypes ( this . rrTypeWindows ) . map ( rtype => "" + rtype ) . join ( "," ) }]` ; } public dataEquals ( record ) { return this . getLowerCasedNextDomainName ( ) === record . getLowerCasedNextDomainName ( ) && deepEqual ( this . rrTypeWindows , record . rrTypeWindows ) ; } private static rrTypesToWindowMap ( rrtypes ) { const rrTypeWindows = [ ] ; for ( const rrtype of rrtypes ) { const windowId = rrtype >> 8 ; let window = undefined ; for ( const window0 of rrTypeWindows ) { if ( window0 . windowId === windowId ) { window = window0 ; break ; } } if ( ! window ) { window = { windowId : windowId , bitMapSize : Math . ceil ( ( rrtype & 0xFF ) / 8 ) , rrtypes : [ rrtype ] , } ; rrTypeWindows . push ( window ) ; } else { window . rrtypes . push ( rrtype ) ; const bitMapSize = Math . ceil ( ( rrtype & 0xFF ) / 8 ) ; if ( bitMapSize > window . bitMapSize ) { window . bitMapSize = bitMapSize ; } } } rrTypeWindows . sort ( ( a , b ) => a . windowId - b . windowId ) ; rrTypeWindows . forEach ( window => window . rrtypes . sort ( ( a , b ) => a - b ) ) ; return rrTypeWindows ; } private static windowsToRRTypes ( windows ) { const rrtypes = [ ] ; for ( const window of windows ) { rrtypes . push ( ... window . rrtypes ) ; } return rrtypes ; } }