From 6ee1769d1343d2540c87717ad4b18307cd921d7d Mon Sep 17 00:00:00 2001 From: Andreas Venizelou Date: Wed, 7 Aug 2024 15:49:10 +0300 Subject: [PATCH] DNS-SD, test and debug --- Protest/Front/dns-sd.js | 30 ++- Protest/Front/hexviewer.js | 168 ++++++++++++++-- Protest/Front/loader.js | 3 +- Protest/Front/mono/nas.svg | 3 + Protest/Front/mono/nvr.svg | 3 + Protest/Front/ui.js | 2 +- Protest/Protocols/Dns.cs | 229 ++++++++++----------- Protest/Protocols/Mdns.cs | 403 +++++++++++++++++++++---------------- 8 files changed, 535 insertions(+), 306 deletions(-) create mode 100644 Protest/Front/mono/nas.svg create mode 100644 Protest/Front/mono/nvr.svg diff --git a/Protest/Front/dns-sd.js b/Protest/Front/dns-sd.js index 4aa5fed1..34c7bbb9 100644 --- a/Protest/Front/dns-sd.js +++ b/Protest/Front/dns-sd.js @@ -205,7 +205,7 @@ class DnsSD extends Console { option.textContent = transportOptions[i]; transportMethodInput.appendChild(option); } - transportMethodInput.value = this.args.transport; + transportMethodInput.value = "UDP"; const Apply = ()=> { this.args.type = recordTypeInput.value; @@ -305,6 +305,11 @@ class DnsSD extends Console { result.className = "tool-result collapsed"; result.textContent = ""; + const status = document.createElement("div"); + status.className = "tool-status"; + status.textContent = ""; + element.appendChild(status); + const remove = document.createElement("div"); remove.className = "tool-remove"; @@ -313,6 +318,7 @@ class DnsSD extends Console { this.hashtable[entryKey] = { element: element, result: result, + status: status, expand: false, list: [] }; @@ -337,7 +343,7 @@ class DnsSD extends Console { this.args.entries.push(entryKey); - try { + /*try*/ { let url = `tools/dnssdlookup?query=${encodeURIComponent(query)}&type=${type ?? this.args.type}&timeout=${this.args.timeout}`; if (this.args.isStandard) url += "&standard=true"; if (this.args.isInverse) url += "&inverse=true"; @@ -362,7 +368,16 @@ class DnsSD extends Console { hexBox.style.backgroundImage = "url(mono/hexviewer.svg?light)"; hexBox.style.cursor = "pointer"; element.appendChild(hexBox); - hexBox.onclick = ()=> new HexViewer({exchange:[{direction:"query", data:json.req},{direction:"response", data:json.res}], protocol:"dns"}); + + hexBox.onclick = ()=>{ + new HexViewer({ + protocol:"mdns", + exchange:[ + {direction:"query", data:json.req}, + {direction:"response", data:json.res} + ] + }); + }; } if (json.error) { @@ -388,12 +403,13 @@ class DnsSD extends Console { name.textContent = json.replace; } + status.style.visibility = "hidden"; + for (let i = 0; i < json.answer.length; i++) { + const box = document.createElement("div"); result.appendChild(box); -console.log(json); - const label = document.createElement("div"); label.textContent = json.answer[i].type; label.style.display = "inline-block"; @@ -415,9 +431,9 @@ console.log(json); box.append(label, string); } } - catch (ex) { + /*catch (ex) { console.error(ex); - } + }*/ } Remove(domain) { diff --git a/Protest/Front/hexviewer.js b/Protest/Front/hexviewer.js index 299bb7a3..9e236c0f 100644 --- a/Protest/Front/hexviewer.js +++ b/Protest/Front/hexviewer.js @@ -279,19 +279,19 @@ class HexViewer extends Window { this.asciiBox.textContent = ""; this.list.textContent = ""; - for (let i = 0; i < exchange.length; i++) { + const PlotPacket = (direction, data)=> { const hexSeparator = document.createElement("div"); - hexSeparator.textContent = exchange[i].direction; + hexSeparator.textContent = direction; hexSeparator.className = "hexviewer-separator"; this.hexBox.appendChild(hexSeparator); const asciiSeparator = document.createElement("div"); - asciiSeparator.textContent = exchange[i].direction; + asciiSeparator.textContent = direction; asciiSeparator.className = "hexviewer-separator"; this.asciiBox.appendChild(asciiSeparator); const listSeparator = document.createElement("div"); - listSeparator.textContent = exchange[i].direction; + listSeparator.textContent = direction; listSeparator.className = "hexviewer-separator"; this.list.appendChild(listSeparator); @@ -301,19 +301,19 @@ class HexViewer extends Window { const charContainer = document.createElement("div"); this.asciiBox.appendChild(charContainer); - for (let j = 0; j < exchange[i].data.length; j++) { + for (let j=0; j{ @@ -328,9 +328,23 @@ class HexViewer extends Window { } switch (protocol) { - case "dns" : this.PopulateDnsLabels(hexContainer, charContainer, exchange[i].data); break; - case "ntp" : this.PopulateNtpLabels(hexContainer, charContainer, exchange[i].data); break; - case "dhcp": this.PopulateDhcpLabels(hexContainer, charContainer, exchange[i].data); break; + case "dns" : this.PopulateDnsLabels(hexContainer, charContainer, data); break; + case "mdns" : this.PopulateMdnsLabels(hexContainer, charContainer, data); break; + case "ntp" : this.PopulateNtpLabels(hexContainer, charContainer, data); break; + case "dhcp" : this.PopulateDhcpLabels(hexContainer, charContainer, data); break; + } + }; + + for (let i=0; i 0) { + if (exchange[i].data[0] instanceof Array) { + for (let j=0; j 0; let options = (stream[2] & 0b01111000) >> 3; let isAuthoritative = (stream[2] & 0b00000100) > 0; - let isTrancated = (stream[2] & 0b00000010) > 0; + let isTruncated = (stream[2] & 0b00000010) > 0; let isRecursive = (stream[2] & 0b00000001) > 0; this.PopulateLabel(`Response: ${isResponse}`, 1, hexContainer, charContainer, 2, 1); this.PopulateLabel(`Options: ${options}`, 1, hexContainer, charContainer, 2, 1); this.PopulateLabel(`Authoritative: ${isAuthoritative}`, 1, hexContainer, charContainer, 2, 1); - this.PopulateLabel(`Trancated: ${isTrancated}`, 1, hexContainer, charContainer, 2, 1); + this.PopulateLabel(`Truncated: ${isTruncated}`, 1, hexContainer, charContainer, 2, 1); this.PopulateLabel(`Recursive: ${isRecursive}`, 1, hexContainer, charContainer, 2, 1); if (isResponse) { @@ -537,7 +551,135 @@ class HexViewer extends Window { count++; } + } + + PopulateMdnsLabels(hexContainer, charContainer, stream, index) { + if (!(stream[index] instanceof Array)) { + this.PopulateDnsLabels(hexContainer, charContainer, stream); + return; + } + + const transactionId = stream[index][0].toString(16).padStart(2,"0") + stream[index][1].toString(16).padStart(2,"0"); + this.PopulateLabel(`Transaction ID: 0x${transactionId}`, 0, hexContainer, charContainer, 0, 2); + + const flags = stream[index][2].toString(16).padStart(2,"0") + stream[index][3].toString(16).padStart(2,"0"); + this.PopulateLabel(`Flags: 0x${flags}`, 0, hexContainer, charContainer, 2, 2); + + let isResponse = (stream[index][2] & 0b10000000) > 0; + let options = (stream[index][2] & 0b01111000) >> 3; + let isAuthoritative = (stream[index][2] & 0b00000100) > 0; + let isTruncated = (stream[index][2] & 0b00000010) > 0; + let isRecursive = (stream[index][2] & 0b00000001) > 0; + this.PopulateLabel(`Response: ${isResponse}`, 1, hexContainer, charContainer, 2, 1); + this.PopulateLabel(`Options: ${options}`, 1, hexContainer, charContainer, 2, 1); + this.PopulateLabel(`Authoritative: ${isAuthoritative}`, 1, hexContainer, charContainer, 2, 1); + this.PopulateLabel(`Truncated: ${isTruncated}`, 1, hexContainer, charContainer, 2, 1); + this.PopulateLabel(`Recursive: ${isRecursive}`, 1, hexContainer, charContainer, 2, 1); + + if (isResponse) { + let isRecursionAvailable = (stream[index][3] & 0b10000000) > 0; + let isAnswerAuthenticated = (stream[index][3] & 0b00100000) > 0; + let nonAuthenticatedData = (stream[index][3] & 0b00010000) > 0; + let replyCode = stream[index][3] & 0b00001111; + this.PopulateLabel(`Recursion is available: ${isRecursionAvailable}`, 1, hexContainer, charContainer, 3, 1); + this.PopulateLabel(`Answer is authenticated: ${isAnswerAuthenticated}`, 1, hexContainer, charContainer, 3, 1); + this.PopulateLabel(`Non authenticated data: ${nonAuthenticatedData}`, 1, hexContainer, charContainer, 3, 1); + this.PopulateLabel(`Reply code: ${replyCode}`, 1, hexContainer, charContainer, 3, 1); + } + + const qCount = stream[index][4] << 8 | stream[index][5]; + this.PopulateLabel(`Questions counter: ${qCount}`, 0, hexContainer, charContainer, 4, 2); + + const anCount = stream[index][6] << 8 | stream[index][7]; + this.PopulateLabel(`Answers counter: ${anCount}`, 0, hexContainer, charContainer, 6, 2); + + const auCount = stream[index][8] << 8 | stream[index][9]; + this.PopulateLabel(`Authority RRs: ${auCount}`, 0, hexContainer, charContainer, 8, 2); + + const adCount = stream[index][10] << 8 | stream[index][11]; + this.PopulateLabel(`Additional RRs: ${adCount}`, 0, hexContainer, charContainer, 10, 2); + let offset = 12; + let count = 0; + while (offset < stream.length && count < qCount + anCount + auCount + adCount) { //answers + let start = offset; + let end = offset; + + switch (stream[offset]) { + case 0xc0: //pointer + end += 2; + break; + + default: + while (end < stream.length && stream[end] !== 0) { + end++; + } + break; + } + + const first = this.PopulateLabel("Name", 1, hexContainer, charContainer, start, end - start); + + offset = end + 1; + + let type = (stream[offset] << 8) | stream[offset+1]; + this.PopulateLabel(`Type: ${type}`, 1, hexContainer, charContainer, offset, 2); + offset += 2; + + let class_ = (stream[offset] << 8) | stream[offset+1]; + this.PopulateLabel(`Class: ${class_}`, 1, hexContainer, charContainer, offset, 2); + offset += 2; + + let ttl = (stream[offset] << 24) | (stream[offset+1] << 16) | (stream[offset+2] << 8) | stream[offset+3]; + this.PopulateLabel(`TTL: ${ttl}`, 1, hexContainer, charContainer, offset, 4); + offset += 4; + + let len = (stream[offset] << 8) | stream[offset+1]; + this.PopulateLabel(`Length: ${len}`, 1, hexContainer, charContainer, offset, 2); + offset += 2; + + let data; + switch (type) { + case 1: //A + if (len === 4) { + data = `${stream[offset]}.${stream[offset+1]}.${stream[offset+2]}.${stream[offset+3]}`; + this.PopulateLabel(data, 1, hexContainer, charContainer, offset, len); + } + break; + + case 28: //AAAA + if (len === 16) { + data = ""; + for (let j = 0; j < 16; j+=2) { + if (j > 0) data += ":"; + data += stream[offset + j].toString(16).padStart(2, "0"); + data += stream[offset + j + 1].toString(16).padStart(2, "0"); + } + this.PopulateLabel(data, 1, hexContainer, charContainer, offset, len); + } + break; + + default: + this.PopulateLabel("Answer", 1, hexContainer, charContainer, offset, len); + break; + } + offset += len; + + end = offset; + + let element; + if (count < qCount + anCount) { + element = this.PopulateLabel("Answer: ", 0, hexContainer, charContainer, start, end - start); + } + else if (count < qCount + anCount + auCount) { + element = this.PopulateLabel("Authority: ", 0, hexContainer, charContainer, start, end - start); + } + else if (count < qCount + anCount + auCount + adCount) { + element = this.PopulateLabel("Additional: ", 0, hexContainer, charContainer, start, end - start); + } + this.list.insertBefore(element, first); + + count++; + } } PopulateNtpLabels(hexContainer, charContainer, stream) { diff --git a/Protest/Front/loader.js b/Protest/Front/loader.js index aa74fd43..de66bb2e 100644 --- a/Protest/Front/loader.js +++ b/Protest/Front/loader.js @@ -17,7 +17,8 @@ const LOADER = { "ip phone" : "mono/ipphone.svg", "lamp" : "mono/lamp.svg", "laptop" : "mono/laptop.svg", - "nas" : "mono/server.svg", + "nas" : "mono/nas.svg", + "nvr" : "mono/nvr.svg", "media player" : "mono/mediaplayer.svg", "music player" : "mono/mediaplayer.svg", "multiprinter" : "mono/multiprinter.svg", diff --git a/Protest/Front/mono/nas.svg b/Protest/Front/mono/nas.svg new file mode 100644 index 00000000..ad43398e --- /dev/null +++ b/Protest/Front/mono/nas.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Protest/Front/mono/nvr.svg b/Protest/Front/mono/nvr.svg new file mode 100644 index 00000000..5d706c02 --- /dev/null +++ b/Protest/Front/mono/nvr.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Protest/Front/ui.js b/Protest/Front/ui.js index 859ab999..4a2c146b 100644 --- a/Protest/Front/ui.js +++ b/Protest/Front/ui.js @@ -326,7 +326,7 @@ const MENU = { { t:"Ping", i:"mono/ping.svg?light", g:"utilities", h:false, f:args=> new Ping(args), k:"roundtrip rtt icmp echo reply" }, { t:"ARP ping", i:"mono/ping.svg?light", g:"utilities", h:true, f:args=> new Ping({entries:[], timeout:500, method:"arp", interval:1000, moveToBottom:false, status:"play"}) }, { t:"DNS lookup", i:"mono/dns.svg?light", g:"utilities", h:false, f:args=> new DnsLookup(args), k:"resolve resolution" }, - { t:"DNS-SD", i:"mono/dns.svg?light", g:"utilities", h:true, f:args=> new DnsSD(args), k:"service discovery multicast lookup resolve resolution" }, + { t:"DNS-SD", i:"mono/dns.svg?light", g:"utilities", h:true, f:args=> new DnsSD(args), k:"mdns service discovery multicast lookup resolve resolution" }, { t:"Trace route", i:"mono/traceroute.svg?light", g:"utilities", h:false, f:args=> new TraceRoute(args), k:"path" }, { t:"TCP port scan", i:"mono/portscan.svg?light", g:"utilities", h:false, f:args=> new PortScan(args), k:"portscan" }, { t:"Locate IP", i:"mono/locate.svg?light", g:"utilities", h:false, f:args=> new LocateIp(args), k:"location" }, diff --git a/Protest/Protocols/Dns.cs b/Protest/Protocols/Dns.cs index a03af09c..a4041980 100644 --- a/Protest/Protocols/Dns.cs +++ b/Protest/Protocols/Dns.cs @@ -248,144 +248,148 @@ public static byte[] Resolve( } Answer[] deconstructed = DeconstructResponse(response, out answerCount, out authorityCount, out additionalCount); - StringBuilder builder = new StringBuilder(); - - builder.Append('{'); - - if (deconstructed.Length > 0 && deconstructed[0].error > 0) { - string errorMessage = deconstructed[0].error switch { - 0 => "no error", - 1 => "query format error", - 2 => "server failure", - 3 => "no such name", - 4 => "function not implemented", - 5 => "refused", - 6 => "name should not exist", - 7 => "RRset should not exist", - 8 => "server not authoritative for the zone", - 9 => "name not in zone", - _ => "unknown error" - }; - builder.Append($"\"error\":\"{errorMessage}\",\"errorcode\": \"{deconstructed[0].error}\","); + return Serialize(query, replaced, response, deconstructed); + } + catch (SocketException ex) { + if (ex.ErrorCode == 10060) { + answerCount = 0; + authorityCount = 0; + additionalCount = 0; + return "{\"error\":\"Connection timed out\",\"errorcode\":\"0\"}"u8.ToArray(); } - - builder.Append("\"req\":["); - for (int i = 0; i < query.Length; i++) { - if (i > 0) builder.Append(','); - builder.Append(query[i]); + else { + answerCount = 0; + authorityCount = 0; + additionalCount = 0; + return "{\"error\":\"unknown error\",\"errorcode\":\"0\"}"u8.ToArray(); } - builder.Append("],"); + } + catch { + answerCount = 0; + authorityCount = 0; + additionalCount = 0; + return "{\"error\":\"unknown error\",\"errorcode\":\"0\"}"u8.ToArray(); + } + } - builder.Append("\"res\":["); - for (int i = 0; i < response.Length; i++) { - if (i > 0) builder.Append(','); - builder.Append(response[i]); - } - builder.Append("],"); + private static byte[] Serialize(byte[] query, string replaced, byte[] response, Answer[] deconstructed) { + StringBuilder builder = new StringBuilder(); - if (replaced is not null) { - builder.Append($"\"replace\":\"{replaced}\","); - } + builder.Append('{'); + + if (deconstructed.Length > 0 && deconstructed[0].error > 0) { + string errorMessage = deconstructed[0].error switch { + 0 => "no error", + 1 => "query format error", + 2 => "server failure", + 3 => "no such name", + 4 => "function not implemented", + 5 => "refused", + 6 => "name should not exist", + 7 => "RRset should not exist", + 8 => "server not authoritative for the zone", + 9 => "name not in zone", + _ => "unknown error" + }; + builder.Append($"\"error\":\"{errorMessage}\",\"errorcode\": \"{deconstructed[0].error}\","); + } - builder.Append("\"answer\":["); - int count = 0; - for (int i = 0; i < deconstructed.Length; i++) { - if (deconstructed[i].isAuthoritative || deconstructed[i].isAdditional) continue; + builder.Append("\"req\":["); + for (int i = 0; i < query.Length; i++) { + if (i > 0) builder.Append(','); + builder.Append(query[i]); + } + builder.Append("],"); - if (count > 0) builder.Append(','); + builder.Append("\"res\":["); + for (int i = 0; i < response.Length; i++) { + if (i > 0) builder.Append(','); + builder.Append(response[i]); + } + builder.Append("],"); - builder.Append('{'); - switch (deconstructed[i].type) { - case RecordType.A: - builder.Append("\"type\":\"A\","); - builder.Append($"\"name\":\"{String.Join(".", deconstructed[i].name)}\","); - break; + if (replaced is not null) { + builder.Append($"\"replace\":\"{replaced}\","); + } - case RecordType.NS: - builder.Append("\"type\":\"NS\","); - builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); - break; + builder.Append("\"answer\":["); + int count = 0; + for (int i = 0; i < deconstructed.Length; i++) { + if (deconstructed[i].isAuthoritative || deconstructed[i].isAdditional) continue; - case RecordType.CNAME: - builder.Append("\"type\":\"CNAME\","); - builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); - break; + if (count > 0) builder.Append(','); - case RecordType.SOA: - builder.Append("\"type\":\"SOA\","); - builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); - break; + builder.Append('{'); + switch (deconstructed[i].type) { + case RecordType.A: + builder.Append("\"type\":\"A\","); + builder.Append($"\"name\":\"{String.Join(".", deconstructed[i].name)}\","); + break; - case RecordType.PTR: - builder.Append("\"type\":\"PTR\","); - builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); - break; + case RecordType.NS: + builder.Append("\"type\":\"NS\","); + builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); + break; - case RecordType.MX: - builder.Append("\"type\":\"MX\","); - builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); - break; + case RecordType.CNAME: + builder.Append("\"type\":\"CNAME\","); + builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); + break; - case RecordType.TXT: - builder.Append("\"type\":\"TXT\","); - builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); - break; + case RecordType.SOA: + builder.Append("\"type\":\"SOA\","); + builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); + break; - case RecordType.AAAA: - builder.Append("\"type\":\"AAAA\","); - if (deconstructed[i].name.Length != 16) { - builder.Append($"\"name\":\"\""); - break; - } + case RecordType.PTR: + builder.Append("\"type\":\"PTR\","); + builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); + break; - builder.Append($"\"name\":\""); - for (int j = 0; j < 16; j += 2) { - if (j > 0) builder.Append(':'); - ushort word = (ushort)((deconstructed[i].name[j] << 8) | deconstructed[i].name[j + 1]); - builder.Append(word.ToString("x4")); - } + case RecordType.MX: + builder.Append("\"type\":\"MX\","); + builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); + break; - builder.Append("\","); - break; + case RecordType.TXT: + builder.Append("\"type\":\"TXT\","); + builder.Append($"\"name\":\"{LabelsToString(deconstructed[i].name, 0, response, out _)}\","); + break; - case RecordType.SRV: - builder.Append("\"type\":\"SRV\","); - builder.Append($"\"name\":\"{String.Join(".", deconstructed[i].name)}\","); + case RecordType.AAAA: + builder.Append("\"type\":\"AAAA\","); + if (deconstructed[i].name.Length != 16) { + builder.Append($"\"name\":\"\""); break; } - builder.Append($"\"ttl\":\"{deconstructed[i].ttl}\""); + builder.Append($"\"name\":\""); + for (int j = 0; j < 16; j += 2) { + if (j > 0) builder.Append(':'); + ushort word = (ushort)((deconstructed[i].name[j] << 8) | deconstructed[i].name[j + 1]); + builder.Append(word.ToString("x4")); + } - builder.Append('}'); + builder.Append("\","); + break; - count++; + case RecordType.SRV: + builder.Append("\"type\":\"SRV\","); + builder.Append($"\"name\":\"{String.Join(".", deconstructed[i].name)}\","); + break; } - builder.Append(']'); + + builder.Append($"\"ttl\":\"{deconstructed[i].ttl}\""); builder.Append('}'); - return Encoding.UTF8.GetBytes(builder.ToString()); - } - catch (SocketException ex) { - if (ex.ErrorCode == 10060) { - answerCount = 0; - authorityCount = 0; - additionalCount = 0; - return "{\"error\":\"Connection timed out\",\"errorcode\":\"0\"}"u8.ToArray(); - } - else { - answerCount = 0; - authorityCount = 0; - additionalCount = 0; - return "{\"error\":\"unknown error\",\"errorcode\":\"0\"}"u8.ToArray(); - } - } - catch { - answerCount = 0; - authorityCount = 0; - additionalCount = 0; - return "{\"error\":\"unknown error\",\"errorcode\":\"0\"}"u8.ToArray(); + count++; } + builder.Append(']'); + + builder.Append('}'); + + return Encoding.UTF8.GetBytes(builder.ToString()); } private static byte[] ConstructQuery( @@ -506,8 +510,7 @@ public static Answer[] DeconstructResponse(byte[] response, out ushort answerCou if (len == 0) break; index += len; } - index += 2; //skip type - index += 2; //skip class + index += 4; //skip type and class } Answer[] result = new Answer[answerCount + authorityCount + additionalCount]; diff --git a/Protest/Protocols/Mdns.cs b/Protest/Protocols/Mdns.cs index e510521a..206d7fb2 100644 --- a/Protest/Protocols/Mdns.cs +++ b/Protest/Protocols/Mdns.cs @@ -3,10 +3,8 @@ using System.IO; using System.Net; using System.Net.Sockets; -using System.Security.Claims; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using static Protest.Protocols.Dns; @@ -48,160 +46,60 @@ public static byte[] Resolve(Dictionary parameters) { } public static byte[] Resolve(string queryString, int timeout = 3000, RecordType type = RecordType.A) { - IPAddress multicast = IPAddress.Parse("224.0.0.251"); - - IPEndPoint remote = new IPEndPoint(multicast, 5353); - IPEndPoint local = new IPEndPoint(IPAddress.Any, 5353); //IPAddress.Any + IPAddress multicastAddress = IPAddress.Parse("224.0.0.251"); + IPEndPoint remoteEndPoint = new IPEndPoint(multicastAddress, 5353); byte[] query = ConstructQuery(queryString, type); - /*try*/ { - using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(local); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicast)); + List receivedData = new List(); + + IPAddress[] nics = IpTools.GetIpAddresses(); + for (int i = 0; i < nics.Length && receivedData.Count == 0; i++) { + if (IPAddress.IsLoopback(nics[i])) { continue; } + + IPAddress localAddress = nics[i]; + IPEndPoint localEndPoint = new IPEndPoint(localAddress, 5353); + + try { + using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + socket.Bind(localEndPoint); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, localAddress)); + socket.ReceiveTimeout = timeout; - socket.SendTo(query, remote); + socket.SendTo(query, remoteEndPoint); - List receivedData = new List(); - DateTime endTime = DateTime.Now.AddMilliseconds(timeout); + DateTime endTime = DateTime.Now.AddMilliseconds(timeout); - while (DateTime.Now < endTime) { - int length = -1; - byte[] reply = new byte[2048]; + while (DateTime.Now <= endTime) { + byte[] reply = new byte[1024]; - try { - length = socket.Receive(reply); - Console.WriteLine(length); - if (length > 0) { - byte[] actualReply = new byte[length]; - Array.Copy(reply, actualReply, length); - receivedData.Add(actualReply); + try { + int length = socket.Receive(reply); + if (length > 0) { + byte[] actualReply = new byte[length]; + Array.Copy(reply, actualReply, length); + receivedData.Add(actualReply); + } } - } - catch (Exception ex) { - Console.WriteLine(ex.ToString()); + catch { } } } + catch {} + } + try { List answers = new List(); - foreach (byte[] response in receivedData) { ushort answerCount, authorityCount, additionalCount; - Answer[] answer = DeconstructResponse(response, out answerCount, out authorityCount, out additionalCount); - if (answerCount == 0 && authorityCount == 0 && additionalCount == 0) { continue; } + Answer[] answer = DeconstructResponse(response, queryString, type, out answerCount, out authorityCount, out additionalCount); answers.AddRange(answer); } - StringBuilder builder = new StringBuilder(); - - builder.Append('{'); - - if (answers.Count > 0 && answers[0].error > 0) { - string errorMessage = answers[0].error switch { - 0 => "no error", - 1 => "query format error", - 2 => "server failure", - 3 => "no such name", - 4 => "function not implemented", - 5 => "refused", - 6 => "name should not exist", - 7 => "RRset should not exist", - 8 => "server not authoritative for the zone", - 9 => "name not in zone", - _ => "unknown error" - }; - builder.Append($"\"error\":\"{errorMessage}\",\"errorcode\": \"{answers[0].error}\","); - } - - builder.Append("\"req\":[],"); - builder.Append("\"res\":[],"); - - builder.Append("\"answer\":["); - int count = 0; - for (int i = 0; i < answers.Count; i++) { - if (answers[i].isAuthoritative || answers[i].isAdditional) continue; - - if (count > 0) builder.Append(','); - - Console.WriteLine("t: " + answers[i].type); - - builder.Append('{'); - switch (answers[i].type) { - case RecordType.A: - builder.Append("\"type\":\"A\","); - builder.Append($"\"name\":\"{String.Join(".", answers[i].name)}\","); - break; - - case RecordType.NS: - builder.Append("\"type\":\"NS\","); - builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); - break; - - case RecordType.CNAME: - builder.Append("\"type\":\"CNAME\","); - builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); - break; - - case RecordType.SOA: - builder.Append("\"type\":\"SOA\","); - builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); - break; - - case RecordType.PTR: - builder.Append("\"type\":\"PTR\","); - builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); - break; - - case RecordType.MX: - builder.Append("\"type\":\"MX\","); - builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); - break; - - case RecordType.TXT: - builder.Append("\"type\":\"TXT\","); - builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); - break; - - case RecordType.AAAA: - builder.Append("\"type\":\"AAAA\","); - if (answers[i].name.Length != 16) { - builder.Append($"\"name\":\"\""); - break; - } - - builder.Append($"\"name\":\""); - for (int j = 0; j < 16; j += 2) { - if (j > 0) builder.Append(':'); - ushort word = (ushort)((answers[i].name[j] << 8) | answers[i].name[j + 1]); - builder.Append(word.ToString("x4")); - } - - builder.Append("\","); - break; - - case RecordType.SRV: - builder.Append("\"type\":\"SRV\","); - builder.Append($"\"name\":\"{String.Join(".", answers[i].name)}\","); - break; - } - - builder.Append($"\"ttl\":\"{answers[i].ttl}\""); - - builder.Append('}'); - - count++; - } - builder.Append(']'); - - builder.Append('}'); - - return Encoding.UTF8.GetBytes(builder.ToString()); - } - /*catch (Exception ex) { - Console.WriteLine($"Error: {ex.Message}"); + return Serialize(query, receivedData, answers); + } catch { return "{\"error\":\"unknown error\",\"errorcode\":\"0\"}"u8.ToArray(); - }*/ + } } private static byte[] ConstructQuery(string queryString, RecordType type) { @@ -219,7 +117,6 @@ private static byte[] ConstructQuery(string queryString, RecordType type) { byte[] query = new byte[len]; - //transaction id Random rand = new Random(); query[0] = (byte)rand.Next(0, 255); @@ -265,18 +162,22 @@ private static byte[] ConstructQuery(string queryString, RecordType type) { return query; } - public static Answer[] DeconstructResponse(byte[] response, out ushort answerCount, out ushort authorityCount, out ushort additionalCount) { + public static Answer[] DeconstructResponse( + byte[] response, + string queryString, + RecordType queryType, + out ushort answerCount, + out ushort authorityCount, + out ushort additionalCount) { + //ushort transactionId = BitConverter.ToUInt16(response, 0); //ushort flags = BitConverter.ToUInt16(response, 2); - ushort questionCount = (ushort)((response[4] << 8) | response[5]); - answerCount = (ushort)((response[6] << 8) | response[7]); - authorityCount = (ushort)((response[8] << 8) | response[9]); - additionalCount = (ushort)((response[10] << 8) | response[11]); + answerCount = (ushort)((response[6] << 8) | response[7]); + authorityCount = (ushort)((response[8] << 8) | response[9]); + additionalCount = (ushort)((response[10] << 8) | response[11]); - bool isResponse = (response[2] & 0b10000000) == 0b10000000; byte error = (byte)(response[3] & 0b00001111); - if (error > 0) { return new Answer[] { new Answer { error = error } }; } @@ -289,53 +190,213 @@ public static Answer[] DeconstructResponse(byte[] response, out ushort answerCou if (len == 0) break; index += len; } - index += 2; //skip type - index += 2; //skip class + index += 4; //skip type and class } - Answer[] result = new Answer[answerCount + authorityCount + additionalCount]; + List result = new List(); - if (result.Length == 0) { - return result; - } + for (int i = 0; i < answerCount + authorityCount + additionalCount; i++) { + Answer ans = new Answer(); - int count = 0; - while (index < response.Length) { - Answer a = new Answer(); + int nameStartIndex = index; - index += 2; //skip name + if ((response[index] & 0xC0) == 0xC0) { + index += 2; + } + else { + while (response[index] != 0) { + index += response[index] + 1; + } + index++; + } - a.type = (RecordType)((response[index] << 8) | response[index + 1]); + ans.type = (RecordType)((response[index] << 8) | response[index + 1]); index += 2; index += 2; //skip class - a.ttl = (response[index] << 24) | (response[index + 1] << 16) | (response[index + 2] << 8) | response[index + 3]; + ans.ttl = (response[index] << 24) | (response[index + 1] << 16) | (response[index + 2] << 8) | response[index + 3]; index += 4; - a.length = (ushort)((response[index] << 8) | response[index + 1]); + ans.length = (ushort)((response[index] << 8) | response[index + 1]); index += 2; - if (a.type == RecordType.MX) { - a.length -= 2; + if (ans.type == RecordType.MX) { + ans.length -= 2; index += 2; //skip preference } - a.name = new byte[a.length]; - for (int i = 0; i < a.length && index < response.Length; i++) { - a.name[i] = response[index++]; + ans.name = new byte[ans.length]; + Array.Copy(response, index, ans.name, 0, ans.length); + index += ans.length; + + //extract name from the response + string responseName = ExtractName(response, nameStartIndex); + if (!responseName.Equals(queryString, StringComparison.OrdinalIgnoreCase) || ans.type != queryType) { + continue; + } + + if (i >= answerCount + authorityCount) { + ans.isAdditional = true; } + else if (i >= answerCount) { + ans.isAuthoritative = true; + } + + result.Add(ans); + } + + return result.ToArray(); + } + + private static string ExtractName(byte[] response, int startIndex) { + StringBuilder name = new StringBuilder(); + int index = startIndex; - if (count > answerCount + authorityCount) { - a.isAdditional = true; + while (response[index] != 0) { + if ((response[index] & 0xC0) == 0xC0) { + //compressed name + int pointer = ((response[index] & 0x3F) << 8) | response[index + 1]; + name.Append(ExtractName(response, pointer)); + break; } - else if (count > answerCount) { - a.isAuthoritative = true; + else { + int length = response[index++]; + for (int i = 0; i < length; i++) { + name.Append((char)response[index++]); + } + name.Append('.'); } + } + if (name.Length > 0) { + name.Length--; //remove trailing dot + } + return name.ToString(); + } + + private static byte[] Serialize(byte[] query, List receivedData, List answers) { + StringBuilder builder = new StringBuilder(); + + builder.Append('{'); + + if (answers.Count > 0 && answers[0].error > 0) { + string errorMessage = answers[0].error switch { + 0 => "no error", + 1 => "query format error", + 2 => "server failure", + 3 => "no such name", + 4 => "function not implemented", + 5 => "refused", + 6 => "name should not exist", + 7 => "RRset should not exist", + 8 => "server not authoritative for the zone", + 9 => "name not in zone", + _ => "unknown error" + }; + builder.Append($"\"error\":\"{errorMessage}\",\"errorcode\": \"{answers[0].error}\","); + } + + builder.Append("\"req\":["); + for (int i = 0; i < query.Length; i++) { + if (i > 0) builder.Append(','); + builder.Append(query[i]); + } + builder.Append("],"); + + builder.Append("\"res\":["); + bool first = true; + for (int i = 0; i < receivedData.Count; i++) { + if (!first) builder.Append(','); + + builder.Append("["); + for (int j = 0; j < receivedData[i].Length; j++) { + if (j > 0) builder.Append(','); + builder.Append(receivedData[i][j]); + } + builder.Append("]"); + first = false; + } + builder.Append("],"); + + + builder.Append("\"answer\":["); + int count = 0; + for (int i = 0; i < answers.Count; i++) { + if (answers[i].isAuthoritative || answers[i].isAdditional) continue; + + if (count > 0) builder.Append(','); + + builder.Append('{'); + + switch (answers[i].type) { + case RecordType.A: + builder.Append("\"type\":\"A\","); + builder.Append($"\"name\":\"{String.Join(".", answers[i].name)}\","); + break; + + case RecordType.NS: + builder.Append("\"type\":\"NS\","); + builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); + break; + + case RecordType.CNAME: + builder.Append("\"type\":\"CNAME\","); + builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); + break; + + case RecordType.SOA: + builder.Append("\"type\":\"SOA\","); + builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); + break; + + case RecordType.PTR: + builder.Append("\"type\":\"PTR\","); + builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); + break; + + case RecordType.MX: + builder.Append("\"type\":\"MX\","); + builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); + break; + + case RecordType.TXT: + builder.Append("\"type\":\"TXT\","); + builder.Append($"\"name\":\"{Dns.LabelsToString(answers[i].name, 0, receivedData[i], out _)}\","); + break; + + case RecordType.AAAA: + builder.Append("\"type\":\"AAAA\","); + if (answers[i].name.Length != 16) { + builder.Append($"\"name\":\"\""); + break; + } + + builder.Append($"\"name\":\""); + for (int j = 0; j < 16; j += 2) { + if (j > 0) + builder.Append(':'); + ushort word = (ushort)((answers[i].name[j] << 8) | answers[i].name[j + 1]); + builder.Append(word.ToString("x4")); + } + + builder.Append("\","); + break; + + case RecordType.SRV: + builder.Append("\"type\":\"SRV\","); + builder.Append($"\"name\":\"{String.Join(".", answers[i].name)}\","); + break; + } + + builder.Append($"\"ttl\":\"{answers[i].ttl}\""); + + builder.Append('}'); - result[count++] = a; + count++; } + builder.Append(']'); - return result; + builder.Append('}'); + return Encoding.UTF8.GetBytes(builder.ToString()); } } \ No newline at end of file