import assert from "assert" ; import deepEqual from "fast-deep-equal" ; import { AddressInfo } from "net" ; import { dnsTypeToString } from "./dns-string-utils" ; import { DNSLabelCoder , NonCompressionLabelCoder } from "./DNSLabelCoder" ; import { Question } from "./Question" ; import "./records" ; import { ResourceRecord } from "./ResourceRecord" ; export const enum OpCode { QUERY = 0 } export const enum RCode { NoError = 0 } export const enum RType { A = 1 , CNAME = 5 , PTR = 12 , TXT = 16 , AAAA = 28 , SRV = 33 , OPT = 41 , NSEC = 47 } export const enum QType { A = 1 , CNAME = 5 , PTR = 12 , TXT = 16 , AAAA = 28 , SRV = 33 , NSEC = 47 , ANY = 255 } export const enum RClass { IN = 1 } export const enum QClass { IN = 1 , ANY = 255 } export const enum PacketType { QUERY = 0 , RESPONSE = 1 } export interface DecodedData < T > { data : T ; readBytes : number ; } export interface OptionalDecodedData < T > { data ? : T ; readBytes : number ; } export interface DNSQueryDefinition { questions : Question [ ] ; answers ? : ResourceRecord [ ] ; additionals ? : ResourceRecord [ ] ; } export interface DNSProbeQueryDefinition { questions : Question [ ] ; authorities ? : ResourceRecord [ ] ; } export interface DNSResponseDefinition { id ? : number ; questions ? : Question [ ] ; answers : ResourceRecord [ ] ; additionals ? : ResourceRecord [ ] ; legacyUnicast ? : boolean ; } function isQuery ( query ) { return "answers" in query ; } function isProbeQuery ( query ) { return "authorities" in query ; } export interface PacketFlags { authoritativeAnswer ? : boolean ; truncation ? : boolean ; recursionDesired ? : boolean ; recursionAvailable ? : boolean ; zero ? : boolean ; authenticData ? : boolean ; checkingDisabled ? : boolean ; } export interface PacketDefinition { id ? : number ; legacyUnicast ? : boolean ; type : PacketType ; opcode ? : OpCode ; flags ? : PacketFlags ; rCode ? : RCode ; questions ? : Question [ ] ; answers ? : ResourceRecord [ ] ; authorities ? : ResourceRecord [ ] ; additionals ? : ResourceRecord [ ] ; } export interface DNSRecord { getEncodingLength ( coder ) : number ; encode ( coder , buffer , offset ) : number ; asString ( ) : string ; } export class DNSPacket { public static readonly UDP_PAYLOAD_SIZE_IPV4 = ( process . env . CIAO_UPS ? parseInt ( process . env . CIAO_UPS ) : 1440 ) ; public static readonly UDP_PAYLOAD_SIZE_IPV6 = ( process . env . CIAO_UPS ? parseInt ( process . env . CIAO_UPS ) : 1440 ) ; private static readonly AUTHORITATIVE_ANSWER_MASK = 0x400 ; private static readonly TRUNCATION_MASK = 0x200 ; private static readonly RECURSION_DESIRED_MASK = 0x100 ; private static readonly RECURSION_AVAILABLE_MASK = 0x80 ; private static readonly ZERO_HEADER_MASK = 0x40 ; private static readonly AUTHENTIC_DATA_MASK = 0x20 ; private static readonly CHECKING_DISABLED_MASK = 0x10 ; private static readonly DNS_PACKET_HEADER_SIZE = 12 ; id : number ; private legacyUnicastEncoding : boolean ; readonly type : PacketType ; readonly opcode : OpCode ; readonly flags : PacketFlags ; readonly rcode : RCode ; readonly questions : Map < string , Question > = new Map ( ) ; readonly answers : Map < string , ResourceRecord > = new Map ( ) ; readonly authorities : Map < string , ResourceRecord > = new Map ( ) ; readonly additionals : Map < string , ResourceRecord > = new Map ( ) ; private estimatedEncodingLength = 0 ; private lastCalculatedLength = 0 ; private lengthDirty = true ; constructor ( definition ) { this . id = definition . id || 0 ; this . legacyUnicastEncoding = definition . legacyUnicast || false ; this . type = definition . type ; this . opcode = definition . opcode || OpCode . QUERY ; this . flags = definition . flags || { } ; this . rcode = definition . rCode || RCode . NoError ; if ( this . type === PacketType . RESPONSE ) { this . flags . authoritativeAnswer = true ; } if ( definition . questions ) { this . addQuestions ( ... definition . questions ) ; } if ( definition . answers ) { this . addAnswers ( ... definition . answers ) ; } if ( definition . authorities ) { this . addAuthorities ( ... definition . authorities ) ; } if ( definition . additionals ) { this . addAdditionals ( ... definition . additionals ) ; } } public static createDNSQueryPacket ( definition , udpPayloadSize = this . UDP_PAYLOAD_SIZE_IPV4 ) { const packets = this . createDNSQueryPackets ( definition , udpPayloadSize ) ; assert ( packets . length === 1 , "Cannot user short method createDNSQueryPacket when query packets are more than one: is " + packets . length ) ; return packets [ 0 ] ; } public static createDNSQueryPackets ( definition , udpPayloadSize = this . UDP_PAYLOAD_SIZE_IPV4 ) { const packets = [ ] ; const packet = new DNSPacket ( { type : PacketType . QUERY , questions : definition . questions , additionals : isQuery ( definition ) ? definition . additionals : undefined , } ) ; packets . push ( packet ) ; if ( packet . getEstimatedEncodingLength ( ) > udpPayloadSize ) { const compressedLength = packet . getEncodingLength ( ) ; if ( compressedLength > udpPayloadSize ) { assert . fail ( "Cannot send query where already the query section is exceeding the udpPayloadSize (" + compressedLength + ">" + udpPayloadSize + ")!" ) ; } } if ( isQuery ( definition ) && definition . answers ) { let currentPacket = packet ; let i = 0 ; const answers = definition . answers . concat ( [ ] ) ; answers . sort ( ( a , b ) => { return a . getEncodingLength ( NonCompressionLabelCoder . INSTANCE ) - b . getEncodingLength ( NonCompressionLabelCoder . INSTANCE ) ; } ) ; while ( i < answers . length ) { for ( ; i < answers . length ; i ++ ) { const answer = answers [ i ] ; const estimatedSize = answer . getEncodingLength ( NonCompressionLabelCoder . INSTANCE ) ; if ( packet . getEstimatedEncodingLength ( ) + estimatedSize <= udpPayloadSize ) { currentPacket . addAnswers ( answer ) ; } else if ( packet . getEncodingLength ( ) + estimatedSize <= udpPayloadSize ) { currentPacket . addAnswers ( answer ) ; } else { if ( currentPacket . questions . size === 0 && currentPacket . answers . size === 0 ) { packet . addAnswers ( answer ) ; } break ; } } if ( i < answers . length ) { currentPacket . flags . truncation = true ; currentPacket = new DNSPacket ( { type : PacketType . QUERY } ) ; packets . push ( currentPacket ) ; } } } else if ( isProbeQuery ( definition ) && definition . authorities ) { packet . addAuthorities ( ... definition . authorities ) ; const compressedLength = packet . getEncodingLength ( ) ; if ( compressedLength > udpPayloadSize ) { assert . fail ( `Probe query packet exceeds the mtu size (${ compressedLength }>${ udpPayloadSize }). Can't split probe queries at the moment!` ) ; } } return packets ; } public static createDNSResponsePacketsFromRRSet ( definition , udpPayloadSize = this . UDP_PAYLOAD_SIZE_IPV4 ) { const packet = new DNSPacket ( { id : definition . id , legacyUnicast : definition . legacyUnicast , type : PacketType . RESPONSE , flags : { authoritativeAnswer : true } , questions : definition . questions , answers : definition . answers , additionals : definition . additionals , } ) ; if ( packet . getEncodingLength ( ) > udpPayloadSize ) { assert . fail ( "Couldn't construct a dns response packet from a rr set which fits in an udp payload sized packet!" ) ; } return packet ; } public canBeCombinedWith ( packet , udpPayloadSize = DNSPacket . UDP_PAYLOAD_SIZE_IPV4 ) { return this . id === packet . id && this . type === packet . type && this . opcode === packet . opcode && deepEqual ( this . flags , packet . flags ) && this . rcode === packet . rcode && this . getEncodingLength ( ) + packet . getEncodingLength ( ) <= udpPayloadSize ; } public combineWith ( packet ) { this . setLegacyUnicastEncoding ( this . legacyUnicastEncoding || packet . legacyUnicastEncoding ) ; this . addRecords ( this . questions , packet . questions . values ( ) ) ; this . addRecords ( this . answers , packet . answers . values ( ) , this . additionals ) ; this . addRecords ( this . authorities , packet . authorities . values ( ) ) ; this . addRecords ( this . additionals , packet . additionals . values ( ) ) ; } public addQuestions ( ... questions ) { return this . addRecords ( this . questions , questions ) ; } public addAnswers ( ... answers ) { return this . addRecords ( this . answers , answers , this . additionals ) ; } public addAuthorities ( ... authorities ) { return this . addRecords ( this . authorities , authorities ) ; } public addAdditionals ( ... additionals ) { return this . addRecords ( this . additionals , additionals ) ; } private addRecords ( recordList , added , removeFromWhenAdded ? ) { let addedAny = false ; for ( const record of added ) { if ( recordList . has ( record . asString ( ) ) ) { continue ; } if ( this . estimatedEncodingLength ) { this . estimatedEncodingLength += record . getEncodingLength ( NonCompressionLabelCoder . INSTANCE ) ; } recordList . set ( record . asString ( ) , record ) ; addedAny = true ; this . lengthDirty = true ; if ( removeFromWhenAdded ) { removeFromWhenAdded . delete ( record . asString ( ) ) ; } } return addedAny ; } public setLegacyUnicastEncoding ( legacyUnicastEncoding ) { if ( this . legacyUnicastEncoding !== legacyUnicastEncoding ) { this . lengthDirty = true ; } this . legacyUnicastEncoding = legacyUnicastEncoding ; } public legacyUnicastEncodingEnabled ( ) { return this . legacyUnicastEncoding ; } private getEstimatedEncodingLength ( ) { if ( this . estimatedEncodingLength ) { return this . estimatedEncodingLength ; } const labelCoder = NonCompressionLabelCoder . INSTANCE ; let length = DNSPacket . DNS_PACKET_HEADER_SIZE ; for ( const record of this . questions . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } for ( const record of this . answers . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } for ( const record of this . authorities . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } for ( const record of this . additionals . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } this . estimatedEncodingLength = length ; return length ; } private getEncodingLength ( coder ? ) { if ( ! this . lengthDirty ) { return this . lastCalculatedLength ; } const labelCoder = coder || new DNSLabelCoder ( this . legacyUnicastEncoding ) ; let length = DNSPacket . DNS_PACKET_HEADER_SIZE ; for ( const record of this . questions . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } for ( const record of this . answers . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } for ( const record of this . authorities . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } for ( const record of this . additionals . values ( ) ) { length += record . getEncodingLength ( labelCoder ) ; } this . lengthDirty = false ; this . lastCalculatedLength = length ; this . estimatedEncodingLength = length ; return length ; } public encode ( ) { const labelCoder = new DNSLabelCoder ( this . legacyUnicastEncoding ) ; const length = this . getEncodingLength ( labelCoder ) ; const buffer = Buffer . allocUnsafe ( length ) ; labelCoder . initBuf ( buffer ) ; let offset = 0 ; buffer . writeUInt16BE ( this . id , offset ) ; offset += 2 ; let flags = ( this . type << 15 ) | ( this . opcode << 11 ) | this . rcode ; if ( this . flags . authoritativeAnswer ) { flags |= DNSPacket . AUTHORITATIVE_ANSWER_MASK ; } if ( this . flags . truncation ) { flags |= DNSPacket . TRUNCATION_MASK ; } if ( this . flags . recursionDesired ) { flags |= DNSPacket . RECURSION_DESIRED_MASK ; } if ( this . flags . recursionAvailable ) { flags |= DNSPacket . RECURSION_AVAILABLE_MASK ; } if ( this . flags . zero ) { flags |= DNSPacket . ZERO_HEADER_MASK ; } if ( this . flags . authenticData ) { flags |= DNSPacket . AUTHENTIC_DATA_MASK ; } if ( this . flags . checkingDisabled ) { flags |= DNSPacket . CHECKING_DISABLED_MASK ; } buffer . writeUInt16BE ( flags , offset ) ; offset += 2 ; buffer . writeUInt16BE ( this . questions . size , offset ) ; offset += 2 ; buffer . writeUInt16BE ( this . answers . size , offset ) ; offset += 2 ; buffer . writeUInt16BE ( this . authorities . size , offset ) ; offset += 2 ; buffer . writeUInt16BE ( this . additionals . size , offset ) ; offset += 2 ; for ( const question of this . questions . values ( ) ) { const length = question . encode ( labelCoder , buffer , offset ) ; offset += length ; } for ( const record of this . answers . values ( ) ) { const length = record . encode ( labelCoder , buffer , offset ) ; offset += length ; } for ( const record of this . authorities . values ( ) ) { const length = record . encode ( labelCoder , buffer , offset ) ; offset += length ; } for ( const record of this . additionals . values ( ) ) { const length = record . encode ( labelCoder , buffer , offset ) ; offset += length ; } assert ( offset === buffer . length , "Bytes written didn't match the buffer size!" ) ; return buffer ; } public static decode ( context , buffer , offset = 0 ) { const labelCoder = new DNSLabelCoder ( ) ; labelCoder . initBuf ( buffer ) ; const id = buffer . readUInt16BE ( offset ) ; offset += 2 ; const flags = buffer . readUInt16BE ( offset ) ; offset += 2 ; const questionLength = buffer . readUInt16BE ( offset ) ; offset += 2 ; const answerLength = buffer . readUInt16BE ( offset ) ; offset += 2 ; const authoritiesLength = buffer . readUInt16BE ( offset ) ; offset += 2 ; const additionalsLength = buffer . readUInt16BE ( offset ) ; offset += 2 ; const questions = [ ] ; const answers = [ ] ; const authorities = [ ] ; const additionals = [ ] ; offset += DNSPacket . decodeList ( context , labelCoder , buffer , offset , questionLength , Question . decode . bind ( Question ) , questions ) ; offset += DNSPacket . decodeList ( context , labelCoder , buffer , offset , answerLength , ResourceRecord . decode . bind ( ResourceRecord ) , answers ) ; offset += DNSPacket . decodeList ( context , labelCoder , buffer , offset , authoritiesLength , ResourceRecord . decode . bind ( ResourceRecord ) , authorities ) ; offset += DNSPacket . decodeList ( context , labelCoder , buffer , offset , additionalsLength , ResourceRecord . decode . bind ( ResourceRecord ) , additionals ) ; assert ( offset === buffer . length , "Didn't read the full buffer (offset=" + offset + ", length=" + buffer . length + ")" ) ; const qr = ( flags >> 15 ) as PacketType ; const opcode = ( ( flags >> 11 ) & 0xf ) as OpCode ; const rCode = ( flags & 0xf ) as RCode ; const packetFlags = { } ; if ( flags & this . AUTHORITATIVE_ANSWER_MASK ) { packetFlags . authoritativeAnswer = true ; } if ( flags & this . TRUNCATION_MASK ) { packetFlags . truncation = true ; } if ( flags & this . RECURSION_DESIRED_MASK ) { packetFlags . recursionDesired = true ; } if ( flags & this . RECURSION_AVAILABLE_MASK ) { packetFlags . recursionAvailable = true ; } if ( flags & this . ZERO_HEADER_MASK ) { packetFlags . zero = true ; } if ( flags & this . AUTHENTIC_DATA_MASK ) { packetFlags . authenticData = true ; } if ( flags & this . CHECKING_DISABLED_MASK ) { packetFlags . checkingDisabled = true ; } return new DNSPacket ( { id : id , type : qr , opcode : opcode , rCode : rCode , flags : packetFlags , questions : questions , answers : answers , authorities : authorities , additionals : additionals , } ) ; } private static decodeList < T > ( context , coder , buffer , offset , length , decoder , destination ) { const oldOffset = offset ; for ( let i = 0 ; i < length ; i ++ ) { const decoded = decoder ( context , coder , buffer , offset ) ; offset += decoded . readBytes ; if ( decoded . data ) { destination . push ( decoded . data ) ; } } return offset - oldOffset ; } public asLoggingString ( udpPayloadSize ? ) { let answerString = "" ; let additionalsString = "" ; for ( const record of this . answers . values ( ) ) { if ( answerString ) { answerString += "," ; } answerString += dnsTypeToString ( record . type ) ; } for ( const record of this . additionals . values ( ) ) { if ( additionalsString ) { additionalsString += "," ; } additionalsString += dnsTypeToString ( record . type ) ; } const optionsStrings = [ ] ; if ( this . legacyUnicastEncodingEnabled ( ) ) { optionsStrings . push ( "U" ) ; } if ( udpPayloadSize ) { optionsStrings . push ( "UPS: " + udpPayloadSize ) ; } const optionsString = optionsStrings . length !== 0 ? ` (${ optionsStrings })` : "" ; return `[${ answerString }] answers and [${ additionalsString }] additionals with size ${ this . getEncodingLength ( ) }B${ optionsString }` ; } }