/// <summary> /// Parses an Resource Record (RR) in a message starting from an offset. /// Moves forward the offset. /// </summary> private static void ParseRR(ReadOnlySpan <byte> response, ref int offset, QueryTransport queryTransport) { string NAME = HandleNAMEinMessage(response, ref offset, queryTransport); var bytesTYPE = response.Slice(offset, 2); var bytesCLASS = response.Slice(offset + 2, 2); var bytesTTL = response.Slice(offset + 4, 4); var bytesRDLENGTH = response.Slice(offset + 8, 2); UInt16 TYPE = BinaryPrimitives.ReadUInt16BigEndian(bytesTYPE); UInt16 CLASS = BinaryPrimitives.ReadUInt16BigEndian(bytesCLASS); UInt32 TTL = BinaryPrimitives.ReadUInt32BigEndian(bytesTTL); UInt16 RDLENGTH = BinaryPrimitives.ReadUInt16BigEndian(bytesRDLENGTH); var bytesRDATA = response.Slice(offset + 10, RDLENGTH); offset += 10 + RDLENGTH; // print info Console.WriteLine($@" NAME: {NAME} TYPE: {TYPE} CLASS: {CLASS} TTL: {TTL} RDLENGTH: {RDLENGTH} RDATA: "); // parse RDATA based on TYPE if (TYPE == 1 || TYPE == 28) // IPv4 or IPv6 { IPAddress address = new IPAddress(bytesRDATA); Console.WriteLine($"\t{address}"); } else if (TYPE == 2 || TYPE == 12) // NS or PTR { string PTRDNAME = NAMEToHostname(bytesRDATA, response, queryTransport); Console.WriteLine($"\t{PTRDNAME}"); } else // print raw data { string RDATA_hex = BitConverter.ToString(bytesRDATA.ToArray()); Console.WriteLine($"\t{RDATA_hex}"); } }
/// <summary> /// The resolver public interface. /// </summary> public static void CubeResolver ( string name, IPAddress dns, QTYPE qTYPE = QTYPE.A, QueryTransport queryTransport = QueryTransport.Udp, string DoHURI = "https://cloudflare-dns.com/dns-query" ) { byte[] QNAME; // if name is an IPAddress, do a PTR query if (IPAddress.TryParse(name, out IPAddress ip)) { qTYPE = QTYPE.PTR; if (ip.AddressFamily == AddressFamily.InterNetwork) { QNAME = IPv4ToQNAME(ip); } else { QNAME = IPv6ToQNAME(ip); } } else // if name is a hostname { QNAME = HostnameToQNAME(name); } byte[] query = MakeQueryDatagram(QNAME, qTYPE, queryTransport); if (queryTransport == QueryTransport.Udp) { if (TryUdpQuery(query, dns, out Span <byte> response)) { ParseResponse(response, query.Length, qTYPE, queryTransport); } else // Udp failed, fallback to Tcp { // change queryTransport and remake query message Console.WriteLine($"UDP query failed. Fallback to TCP."); queryTransport = QueryTransport.Tcp; query = MakeQueryDatagram(QNAME, qTYPE, queryTransport); } } if (queryTransport == QueryTransport.Tcp) { if (TryTcpQuery(query, dns, out Span <byte> response)) { ParseResponse(response, query.Length, qTYPE, queryTransport); } else // Tcp failed, fallback to DoT { // change queryTransport and remake query message Console.WriteLine($"TCP query failed. Fallback to DNS over TLS."); queryTransport = QueryTransport.DoT; query = MakeQueryDatagram(QNAME, qTYPE, queryTransport); } } if (queryTransport == QueryTransport.DoT) { if (TryDoTQuery(query, dns, out Span <byte> response)) { ParseResponse(response, query.Length, qTYPE, queryTransport); } else // DoT failed { Console.WriteLine($"DNS over TLS query failed."); } } if (queryTransport == QueryTransport.DoH) { if (TryDoHQuery(query, out Span <byte> response, DoHURI)) { ParseResponse(response, query.Length, qTYPE, queryTransport); } else // DoH failed { Console.WriteLine($"DNS over HTTPS query failed."); } } }
/// <summary> /// Parses the response message. /// </summary> private static void ParseResponse(ReadOnlySpan <byte> response, int queryLength, QTYPE qTYPE, QueryTransport queryTransport) { int offset = 0, tcpMsgLength = 0, questionOffset = 0; // Tcp & DoT pre-processing if (queryTransport == QueryTransport.Tcp || queryTransport == QueryTransport.DoT) { // save TcpMsgLength and remove it byte[] bytesTcpMsgLength = new byte[4]; Array.Copy(response.Slice(start: 0, length: 2).ToArray(), 0, bytesTcpMsgLength, 2, 2); tcpMsgLength = BinaryPrimitives.ReadInt32BigEndian(bytesTcpMsgLength); offset = 2; } // read header var bytesID = response.Slice(start: offset, length: 2); var bytesHeaderStuff = response.Slice(start: offset + 2, length: 2); var bytesQDCOUNT = response.Slice(start: offset + 4, length: 2); var bytesANCOUNT = response.Slice(start: offset + 6, length: 2); var bytesNSCOUNT = response.Slice(start: offset + 8, length: 2); var bytesARCOUNT = response.Slice(start: offset + 10, length: 2); // parse header int QR = (bytesHeaderStuff[0] & 0b1000_0000) >> 7; int OPCODE = (bytesHeaderStuff[0] & 0b0111_1000) >> 3; int AA = (bytesHeaderStuff[0] & 0b0000_0100) >> 2; int TC = (bytesHeaderStuff[0] & 0b0000_0010) >> 1; int RD = bytesHeaderStuff[0] & 0b0000_0001; int RA = (bytesHeaderStuff[1] & 0b1000_0000) >> 7; int Z = (bytesHeaderStuff[1] & 0b0111_0000) >> 4; int RCODE = bytesHeaderStuff[1] & 0b0000_1111; UInt16 QDCOUNT = BinaryPrimitives.ReadUInt16BigEndian(bytesQDCOUNT); UInt16 ANCOUNT = BinaryPrimitives.ReadUInt16BigEndian(bytesANCOUNT); UInt16 NSCOUNT = BinaryPrimitives.ReadUInt16BigEndian(bytesNSCOUNT); UInt16 ARCOUNT = BinaryPrimitives.ReadUInt16BigEndian(bytesARCOUNT); // checks /*if (bytesID[0] != 0x20 || bytesID[1] != 0x40 || QR != 1 || OPCODE != 0 || TC != 0 || RCODE != 0 || QDCOUNT != 1) || { || Console.WriteLine($"Warning: header checks failed."); || }*/ // read question // length of QNAME is unknown // look for 0x00 questionOffset = offset + 12; // offset is relative to response offset = questionOffset + MemoryExtensions.IndexOf(response.Slice(questionOffset), (byte)0x00); if (offset == -1) { Console.WriteLine("Error parsing response: invalid QNAME."); return; } var bytesQNAME = response.Slice(start: questionOffset, length: offset - questionOffset + 1); var bytesQTYPE = response.Slice(start: offset + 1, length: 2); var bytesQCLASS = response.Slice(start: offset + 3, length: 2); // parse question string QNAME = NAMEToHostname(bytesQNAME, response, queryTransport); UInt16 QTYPE = BinaryPrimitives.ReadUInt16BigEndian(bytesQTYPE); UInt16 QCLASS = BinaryPrimitives.ReadUInt16BigEndian(bytesQCLASS); // print info Console.WriteLine($@"Received bytes: {response.Length} ####### HEADER ####### ID: {BinaryPrimitives.ReadUInt16BigEndian(bytesID)} QR: {QR} OPCODE: {OPCODE} AA: {AA} TC: {TC} RD: {RD} RA: {RA} Z: {Z} RCODE: {RCODE} QDCOUNT: {QDCOUNT} ANCOUNT: {ANCOUNT} NSCOUNT: {NSCOUNT} ARCOUNT: {ARCOUNT} ####### QUESTION ####### QNAME: {QNAME} QTYPE: {QTYPE} QCLASS: {QCLASS}"); offset += 5; // Parse all RRs for (int i = 0; i < ANCOUNT; i++) { Console.WriteLine($"\n####### ANSWER #{i} #######"); ParseRR(response, ref offset, queryTransport); } for (int i = 0; i < NSCOUNT; i++) { Console.WriteLine($"\n####### AUTHORITY #{i} #######"); ParseRR(response, ref offset, queryTransport); } for (int i = 0; i < ARCOUNT; i++) { Console.WriteLine($"\n####### ADDITIONAL #{i} #######"); ParseRR(response, ref offset, queryTransport); } }
/// <summary> /// Finds the NAME in a message starting from an offset. /// Precisely extracts the NAME. /// Translates the NAME to a string. /// Moves forward the offset and returns the string. /// </summary> private static string HandleNAMEinMessage(ReadOnlySpan <byte> response, ref int offset, QueryTransport queryTransport) { int oldOffset = offset; ReadOnlySpan <byte> bytesNAME; if (response[offset] >= 0xc0) // domain name is compressed { bytesNAME = response.Slice(offset, 2); offset += 2; } else { // offset is relative to response offset = offset + MemoryExtensions.IndexOf(response.Slice(offset), (byte)0x00); bytesNAME = response.Slice(oldOffset, offset - oldOffset + 1); offset++; } return(NAMEToHostname(bytesNAME, response, queryTransport)); }
/// <summary> /// Translates the NAME in a DNS response to a hostname string. /// Can handle extra bytes after the actual NAME. /// </summary> private static string NAMEToHostname(ReadOnlySpan <byte> NAME, ReadOnlySpan <byte> DnsMessage, QueryTransport queryTransport) { int pos = 0; StringBuilder hostname = new StringBuilder(256); while (NAME[pos] != 0x00) { // if compressed if (NAME[pos] >= 0xc0) { // extract offset into a new array var byteOffset = NAME.Slice(pos, 2).ToArray(); byteOffset[0] &= 0b0011_1111; int offset = BinaryPrimitives.ReadUInt16BigEndian(byteOffset); // recursively get hostname and append to hostname if (queryTransport == QueryTransport.Tcp || queryTransport == QueryTransport.DoT) { offset += 2; // in a Tcp and DoT message, offset is relative to the header ID. } hostname.Append(NAMEToHostname(DnsMessage.Slice(offset, DnsMessage.Length - offset), DnsMessage, queryTransport)); hostname.Append('.'); // reached the end of NAME break; } else { hostname.Append(Encoding.ASCII.GetString(NAME.Slice(start: pos + 1, length: NAME[pos]))); hostname.Append('.'); pos += (NAME[pos] + 1); } } // remove the last '.' if (hostname.Length != 0) { hostname.Remove(hostname.Length - 1, 1); } return(hostname.ToString()); }
/// <summary> /// Makes a query datagram containing a header and a question. /// Returns a byte array. /// </summary> private static byte[] MakeQueryDatagram(byte[] QNAME, QTYPE qTYPE, QueryTransport queryTransport) { byte[] query; byte[] header = new byte[12]; Span <byte> headerSpan = new Span <byte>(header); byte[] question = new byte[QNAME.Length + 4]; // BEGIN Header // ID // generate random ID rnd.NextBytes(headerSpan.Slice(start: 0, length: 2)); // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ // |QR| Opcode |AA|TC|RD|RA| Z | RCODE | header[2] = 0x01; // RD == 1 header[3] = 0x00; // QDCOUNT // No support for multiple questions in one query // as it's not supported by any DNS server implementation header[4] = 0x00; header[5] = 0x01; // ANCOUNT header[6] = 0x00; header[7] = 0x00; // NSCOUNT header[8] = 0x00; header[9] = 0x00; // ARCOUNT header[10] = 0x00; header[11] = 0x00; // END Header // BEGIN Question Array.Copy(QNAME, question, QNAME.Length); // QTYPE byte[] qTYPE4Bytes = new byte[4]; BinaryPrimitives.WriteInt32BigEndian(qTYPE4Bytes, (int)qTYPE); question[QNAME.Length] = qTYPE4Bytes[2]; question[QNAME.Length + 1] = qTYPE4Bytes[3]; // QCLASS: IN question[QNAME.Length + 2] = 0x00; question[QNAME.Length + 3] = 0x01; // END Question // make query message based on queryTransport if (queryTransport == QueryTransport.Udp || queryTransport == QueryTransport.DoH) // Udp || DoH { query = new byte[header.Length + question.Length]; Array.Copy(header, query, header.Length); Array.Copy(question, 0, query, header.Length, question.Length); } else // Tcp || DoT { query = new byte[2 + header.Length + question.Length]; // Tcp Message Length byte[] tcpMsgLengthBytes = new byte[4]; BinaryPrimitives.WriteInt32BigEndian(tcpMsgLengthBytes, header.Length + question.Length); Array.Copy(tcpMsgLengthBytes, 2, query, 0, 2); Array.Copy(header, 0, query, 2, 12); // header.Length == 12 Array.Copy(question, 0, query, 14, question.Length); // 2 + 12 == 14 } return(query); }