Add DNS oracle protocol#917
Conversation
shargon
left a comment
There was a problem hiding this comment.
I think that it's better to define the dns server, and make a dns query, instead of http query
We need DNS over HTTPS. |
Then it's DoH no Dns, we should rename the oracle protocol |
DoH is very commom see https://www.rfc-editor.org/rfc/rfc8484.html Can we follow rfc8484? |
Co-authored-by: Will <201105916+Wi1l-B0t@users.noreply.github.com>
Co-authored-by: Will <201105916+Wi1l-B0t@users.noreply.github.com>
- Replace application/dns-json with standard application/dns-message - Implement DNS wire format (RFC 1035) for query/response encoding - Use HTTP POST method per RFC 8484 specification - Add DNS name compression pointer support - Support user-specified authority in URI (dns://resolver/domain) - Fix CryptographicException handling in BuildPublicKey - Move Accept header to constructor - Add comprehensive unit tests for wire format handling - Add integration tests for Cloudflare, Google, and Quad9 DoH endpoints - Update documentation with RFC 8484 compliance details
|
|
UT Failed. |
|
@Jim8y any progress with this? |
Co-authored-by: Christopher Schuchardt <8141309+cschuchardt88@users.noreply.github.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #917 +/- ##
=========================================
Coverage ? 47.79%
=========================================
Files ? 275
Lines ? 16399
Branches ? 2135
=========================================
Hits ? 7838
Misses ? 8002
Partials ? 559 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a DNS-over-HTTPS (RFC 8484) oracle protocol for dns: URIs (RFC 4501), wiring it into the OracleService plugin alongside the existing https:// and neofs:// protocols. Resolved records are returned as NeoVM-serialized Struct envelopes for in-contract consumption.
Changes:
- Add
OracleDnsProtocolimplementing DNS wire-format query/parse (A/AAAA/TXT/CERT and others) over DoH POST. - Add
DnsSettings, default config entry, and register thednsscheme inOracleService.Start; route DNS results as raw stack-item bytes (filter not supported). - Add user docs and an extensive unit/integration test suite (URI parsing, RCODE handling, name compression, multiple answers, etc.).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/OracleService/Protocols/OracleDnsProtocol.cs | New DoH oracle protocol with RFC 1035/8484 wire-format parsing and result serialization. |
| plugins/OracleService/OracleService.cs | Registers the dns protocol and adds DNS-specific result handling, logging, and base64 decoding. |
| plugins/OracleService/OracleSettings.cs | Adds DnsSettings (EndPoint, Timeout) and exposes it on OracleSettings. |
| plugins/OracleService/OracleService.json | Adds default Dns configuration block. |
| tests/Neo.Plugins.OracleService.Tests/UT_OracleDnsProtocol.cs | Unit + opt-in integration tests covering URI parsing, record types, error mapping, and live DoH endpoints. |
| docs/oracle-dns-protocol.md | User-facing documentation describing config, URI syntax, response schema, and contract usage. |
Comments suppressed due to low confidence (6)
plugins/OracleService/Protocols/OracleDnsProtocol.cs:636
- The
queryParametersparameter is assigned via??=but never used afterwards. Either remove the parameter (it's only useful inside the function) or use it (e.g., to validate query keys or extract them). As written, the public-looking API takes a parameter that's effectively ignored, which is misleading for callers (and at least one caller inProcessAsyncdoes pre-parse and passquery).
internal static string BuildQueryName(Uri uri, NameValueCollection? queryParameters = null)
{
queryParameters ??= ParseQueryString(uri.Query);
string dnsName = NormalizeDnsName(uri.GetComponents(UriComponents.Path, UriFormat.Unescaped));
if (string.IsNullOrEmpty(dnsName))
throw new FormatException("dns: URI must include a dnsname.");
return dnsName;
}
plugins/OracleService/Protocols/OracleDnsProtocol.cs:529
DecodeDnsNameaccepts forward-pointing or self-referential compression pointers as long asjumpCountstays under 128. A malicious response can use up to 128 jumps to produce a name many KB long (each label up to 63 bytes) and exhaust memory before the cap triggers, or cause O(n²) parsing. RFC 1035 implementations typically also require pointers to point strictly backwards (to a previously-decoded offset) to prevent forward loops. Consider enforcingpointer < currentOffsetand a tighter byte limit on the assembled name length (≤255 octets per RFC 1035).
private static (string Name, int NewOffset) DecodeDnsName(byte[] data, int offset)
{
StringBuilder name = new();
int originalOffset = offset;
bool jumped = false;
int jumpCount = 0;
const int maxJumps = 128; // Prevent infinite loops
while (offset < data.Length)
{
byte length = data[offset];
if (length == 0)
{
offset++;
break;
}
// Check for compression pointer (top 2 bits set)
if ((length & 0xC0) == 0xC0)
{
if (offset + 1 >= data.Length)
throw new FormatException("DNS name compression pointer truncated.");
if (++jumpCount > maxJumps)
throw new FormatException("DNS name compression loop detected.");
int pointer = ((length & 0x3F) << 8) | data[offset + 1];
if (!jumped)
{
originalOffset = offset + 2;
jumped = true;
}
offset = pointer;
continue;
}
offset++;
if (offset + length > data.Length)
throw new FormatException("DNS label extends beyond message.");
if (name.Length > 0)
name.Append('.');
name.Append(Encoding.ASCII.GetString(data, offset, length));
offset += length;
}
return (name.ToString(), jumped ? originalOffset : offset);
}
plugins/OracleService/Protocols/OracleDnsProtocol.cs:595
FormatTxtRecordsilentlybreaks when a length-prefix would overrun the buffer, swallowing the corruption and returning a partial (potentially empty) string while still reportingSuccess. Oracle responses are consensus-critical: returning partial data on malformed input means different nodes parsing in the same way still get an "OK" stack item but downstream consumers can't tell the record was truncated. Consider throwing aFormatExceptionhere so the protocol returnsOracleResponseCode.Errorinstead of silently fabricating an answer.
private static string FormatTxtRecord(byte[] rdata)
{
StringBuilder result = new();
int offset = 0;
while (offset < rdata.Length)
{
int length = rdata[offset++];
if (offset + length > rdata.Length)
break;
if (result.Length > 0)
result.Append(' ');
result.Append('"');
result.Append(Encoding.UTF8.GetString(rdata, offset, length));
result.Append('"');
offset += length;
}
return result.ToString();
}
plugins/OracleService/Protocols/OracleDnsProtocol.cs:475
- Each
ProcessAsynccall sends a fresh DNS request with a random 16-bit ID, but the protocol does not verify that the response ID matches the query ID, nor that the response's question section matches the query name/type. RFC 8484 makes spoofing harder than UDP, but you're still trusting the upstream resolver — any HTTPS server in the trust chain could return arbitrary records under a different name. Since oracle nodes must reach consensus on the answer, mismatched responses (e.g., from caches or misconfigured proxies) silently get accepted. Consider validatingmessage.Id == requestIdand that the question section names the requested owner/type.
private static DnsMessage ParseDnsResponse(byte[] data)
{
if (data is null || data.Length < DnsHeaderSize)
throw new FormatException("DNS response too short.");
DnsMessage message = new()
{
Id = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(0, 2)),
Flags = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(2, 2))
};
ushort qdCount = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(4, 2));
ushort anCount = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(6, 2));
int offset = DnsHeaderSize;
// Skip question section
for (int i = 0; i < qdCount; i++)
{
offset = SkipDnsName(data, offset);
offset += 4; // QTYPE (2) + QCLASS (2)
}
// Parse answer section
for (int i = 0; i < anCount; i++)
{
(string name, int newOffset) = DecodeDnsName(data, offset);
offset = newOffset;
if (offset + 10 > data.Length)
throw new FormatException("DNS response truncated in answer section.");
ushort type = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(offset, 2));
ushort cls = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(offset + 2, 2));
uint ttl = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset + 4, 4));
ushort rdLength = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(offset + 8, 2));
offset += 10;
if (offset + rdLength > data.Length)
throw new FormatException("DNS response truncated in RDATA.");
byte[] rdata = new byte[rdLength];
Array.Copy(data, offset, rdata, 0, rdLength);
offset += rdLength;
message.Answers.Add(new DnsResourceRecord
{
Name = name,
Type = type,
Class = cls,
Ttl = ttl,
RData = rdata
});
}
return message;
}
plugins/OracleService/Protocols/OracleDnsProtocol.cs:200
- For DoH consensus to work across oracle nodes, all nodes must produce byte-identical results from the same
dns:URL. Several pieces of this design make that fragile:
- When the URI omits an authority, each node uses its own locally configured
Dns.EndPoint, so different operators will hit different resolvers and may get differentAnswersordering, TTLs, or even contents (especially for geo-routed records). The answer set is sorted in the order the resolver returned it. - The
Ttlis included in the serialized struct verbatim. Two nodes querying seconds apart will see different TTL countdowns, breaking consensus on the result bytes. - Multi-answer responses (e.g., A records) are not sorted, so different resolvers' answer orderings will produce different serialized bytes.
This makes dns: requests effectively non-consensus-safe in practice. Consider either: (a) requiring the URL to specify the authority and normalizing/canonicalizing TTLs (e.g., zero them out), and sorting answers; or (b) documenting prominently that operators must agree on Dns:EndPoint and that consensus is best-effort.
ResultAnswer[] answers = dnsResponse.Answers
.Select(a => new ResultAnswer
{
Name = a.Name.TrimEnd('.'),
Type = GetRecordTypeLabel(a.Type),
Ttl = a.Ttl,
Data = FormatRData(a.Type, a.RData)
})
.ToArray();
byte[] stackBytes = SerializeStackItemEnvelope(queryName, recordTypeLabel, answers);
if (stackBytes.Length > OracleResponse.MaxResultSize)
return (OracleResponseCode.ResponseTooLarge, null);
return (OracleResponseCode.Success, Convert.ToBase64String(stackBytes));
}
plugins/OracleService/Protocols/OracleDnsProtocol.cs:626
OracleDnsProtocol.Configure()callsEnsureConfigured(force: true)which re-readsOracleSettings.Default.Dnsand re-setsclient.Timeout. However,HttpClient.Timeoutcannot be changed after the first request has been sent — it throwsInvalidOperationException. SinceConfigureis called by the plugin lifecycle after the client has potentially been used (e.g., on hot-reload), this can crash the service. The siblingOracleHttpsProtocol.Configurealso setsclient.Timeout, but in that file the headers are cleared first as well — same potential issue. Consider either creating a newHttpClientper Configure call or using a per-requestCancellationTokenSourcewith the configured timeout instead ofHttpClient.Timeout.
private void EnsureConfigured(bool force = false)
{
if (configured && !force)
return;
lock (syncRoot)
{
if (configured && !force)
return;
var dnsSettings = OracleSettings.Default?.Dns ?? throw new InvalidOperationException("DNS settings are not loaded.");
endpoint = dnsSettings.EndPoint;
client.Timeout = dnsSettings.Timeout;
configured = true;
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Testing