Skip to content

Commit

Permalink
Adding NSEC3 records (#119)
Browse files Browse the repository at this point in the history
Added NSEC3 resource record
Added NSEC3PARAM resource record
Added a Base32 en-/de-coder with the hex version which is used by NSEC3
Also fixed a few things in the bitmap reader implementation of the NSEC record and wrote a writer part, too, to test multi windowed bitmaps properly. The algorithm is inspired by the c++ implementation of the unbound resolver.
  • Loading branch information
MichaCo authored May 24, 2021
1 parent 4ce4a4e commit 7239fe4
Show file tree
Hide file tree
Showing 15 changed files with 748 additions and 64 deletions.
25 changes: 7 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ var ip = record?.Address;
* Minimum TTL setting to overrule the result's TTL and always cache the responses for at least that time. (Even very low value, like a few milliseconds, do make a huge difference if used in high traffic low latency scenarios)
* Maximum TTL to limit cache duration
* Cache can be disabled
* Nameserver auto discovery. If no servers are explicitly configured, DnsClient will try its best to resolve them based on your local system configuration.
This includes DNS servers configured via network interfaces or even via Windows specific [NRPT policies](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745).
* Multiple DNS endpoints can be configured. DnsClient will use them in random or sequential order (configurable), with re-tries.
* Configurable retry of queries
* Optional audit trail of each response and exception
Expand All @@ -42,32 +44,19 @@ var ip = record?.Address;

### Supported resource records

* A, AAAA, NS, CNAME, SOA, MB, MG, MR, WKS, HINFO, MINFO, MX, RP, TXT, AFSDB, URI, CAA, NULL, SSHFP, TLSA, RRSIG, NSEC, DNSKEY, DS
* A, AAAA, NS, CNAME, SOA, MB, MG, MR, WKS, HINFO, MINFO, MX, RP, TXT, AFSDB, URI, CAA, NULL, SSHFP, TLSA, RRSIG, NSEC, NSEC3, NSEC3PARAM, DNSKEY, DS
* PTR for reverse lookups
* SRV for service discovery. `LookupClient` has some extensions to help with that.
* AXFR zone transfer (as per spec, LookupClient has to be set to TCP mode only for this type. Also, the result depends on if the DNS server trusts your current connection)

## Build from Source

The solution requires a .NET Core 3.x SDK and the [.NET 4.7.1 Dev Pack](https://www.microsoft.com/net/download/dotnet-framework/net471) being installed.

Just clone the repository and open the solution in Visual Studio 2017/2019.

The unit tests don't require any additional setup right now.

If you want to test the different record types, there are config files for Bind under tools.
Just [download Bind](https://www.isc.org/downloads/) for Windows and copy the binaries to tools/BIND, then run bind.cmd.
If you are running this on Linux, you can use my config files and replace the default ones if you want.

Now, you can use **samples/MiniDig** to query the local DNS server.
The following should return many different resource records:

``` cmd
dotnet run -s localhost mcnet.com any
```
To build and contribute to this project, you must have the latest [.NET 5 SDK](https://dotnet.microsoft.com/download) installed.
Just clone the repository and open the solution in Visual Studio 2019.

## Examples

* See [MiniDig](https://github.com/MichaCo/DnsClient.NET/tree/dev/samples/MiniDig)'s readme for what this example command line tool can do.
* More documentation and a simple query window on http://dnsclient.michaco.net
* The [Samples](https://github.com/MichaCo/DnsClient.NET.Samples) repository (there might be more in the future).
* [MiniDig](https://github.com/MichaCo/DnsClient.NET/tree/dev/samples/MiniDig)

55 changes: 49 additions & 6 deletions samples/MiniDig/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,66 @@ It is supposed to work similar to the well-known `dig` command line tool on Linu
## How to Build/Run it
To run it, open a command line windows, or bash, navigate to `/Samples/MiniDig` and run `dotnet restore` and `dotnet run`.

MiniDig is multi targeted for now, which means, when you use `dotnet run` you have to specify a framework
`-f NetCoreApp2.0` or `-f net472` for example.
MiniDig is targeting .NET5 and will run on any supported platform as long as the .NET5 SDK or runtime is installed.

## Examples
`dotnet run -f netcoreapp2.0 google.com ANY` to query for google.com
`dotnet run google.com ANY` to query for google.com

Example output:
```csharp
; <<>> MiniDiG 1.0.0.0 Microsoft Windows 10.0.19042 <<>> google.com any
; Servers: 192.168.0.2:53
; (1 server found)
;; Got answer:
;; ->>HEADER<<- opcode: Query, status: No Error, id: 39207
;; flags: qr rd ra; QUERY: 1, ANSWER: 22, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; UDP: 1472; code: NoError
;; QUESTION SECTION:
google.com. IN ANY

;; ANSWER SECTION:
google.com. 1195 IN A 216.58.207.142
google.com. 1195 IN AAAA 2a00:1450:4016:806::200e
google.com. 1195 IN SOA ns1.google.com. dns-admin.google.com. 375442030 900 900 1800 60
google.com. 345595 IN NS ns2.google.com.
google.com. 345595 IN NS ns4.google.com.
google.com. 345595 IN NS ns3.google.com.
google.com. 345595 IN NS ns1.google.com.
google.com. 86395 IN CAA 0 issue "pki.goog"
google.com. 3595 IN TXT "docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"
google.com. 3595 IN TXT "docusign=1b0a6754-49b1-4db5-8540-d2c12664b289"
google.com. 3595 IN TXT "facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95"
google.com. 3595 IN TXT "google-site-verification=TV9-DBe4R80X4v0M4U_bd_J9cpOJM0nikft0jAgjmsQ"
google.com. 3595 IN TXT "MS=E4A68B9AB2BB9670BCE15412F62916164C0B20BB"
google.com. 3595 IN TXT "globalsign-smime-dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8="
google.com. 3595 IN TXT "google-site-verification=wD8N7i1JTNTkezJ49swvWW48f8_9xveREV4oB-0Hf5o"
google.com. 3595 IN TXT "apple-domain-verification=30afIBcvSuDV2PLX"
google.com. 3595 IN TXT "v=spf1 include:_spf.google.com ~all"
google.com. 1195 IN MX 10 aspmx.l.google.com.
google.com. 1195 IN MX 20 alt1.aspmx.l.google.com.
google.com. 1195 IN MX 30 alt2.aspmx.l.google.com.
google.com. 1195 IN MX 40 alt3.aspmx.l.google.com.
google.com. 1195 IN MX 50 alt4.aspmx.l.google.com.

;; Query time: 58 msec
;; SERVER: 192.168.0.2#53
;; WHEN: Mon May 24 21:52:45 Z 2021
;; MSG SIZE rcvd: 922
```

If nothing else is specified, it uses the DNS server configured for your local network adapter.
To specify a different server, use the `-s` switch, for example:

`dotnet run -f netcoreapp2.0 -s 8.8.8.8 google.com` to use the public Google name server.
`dotnet run -s 8.8.8.8 google.com` to use the public Google name server.


`dotnet run -f netcoreapp2.0 -s 127.0.0.1#8600` to also specify a custom port.
`dotnet run -s 127.0.0.1#8600` to also specify a custom port.


## Performance Testing
One subcommand `perf` can be used to run performance tests

## Random Testing
The `random` sub command does a lookup on a list of domain names found in names.txt.
The `random` sub command does a lookup on a list of domain names found in names.txt.
32 changes: 32 additions & 0 deletions src/DnsClient/DnsRecordFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ public DnsResourceRecord GetRecord(ResourceRecordInfo info)
result = ResolveDnsKeyRecord(info);
break;

case ResourceRecordType.NSEC3: // 50
result = ResolveNSec3Record(info);
break;

case ResourceRecordType.NSEC3PARAM: // 51
result = ResolveNSec3ParamRecord(info);
break;

case ResourceRecordType.TLSA: // 52
result = ResolveTlsaRecord(info);
break;
Expand Down Expand Up @@ -306,6 +314,30 @@ private DnsResourceRecord ResolveNSecRecord(ResourceRecordInfo info)
return new NSecRecord(info, nextName, bitMaps);
}

private DnsResourceRecord ResolveNSec3Record(ResourceRecordInfo info)
{
var startIndex = _reader.Index;
var hashAlgorithm = _reader.ReadByte();
var flags = _reader.ReadByte();
var iterations = _reader.ReadUInt16NetworkOrder();
var saltLength = _reader.ReadByte();
var salt = _reader.ReadBytes(saltLength).ToArray();
var nextOwnerLength = _reader.ReadByte();
var nextOwnersName = _reader.ReadBytes(nextOwnerLength).ToArray();
var bitMaps = _reader.ReadBytesToEnd(startIndex, info.RawDataLength).ToArray();
return new NSec3Record(info, hashAlgorithm, flags, iterations, salt, nextOwnersName, bitMaps);
}

private DnsResourceRecord ResolveNSec3ParamRecord(ResourceRecordInfo info)
{
var hashAlgorithm = _reader.ReadByte();
var flags = _reader.ReadByte();
var iterations = _reader.ReadUInt16NetworkOrder();
var saltLength = _reader.ReadByte();
var salt = _reader.ReadBytes(saltLength).ToArray();
return new NSec3ParamRecord(info, hashAlgorithm, flags, iterations, salt);
}

private DnsResourceRecord ResolveDnsKeyRecord(ResourceRecordInfo info)
{
var startIndex = _reader.Index;
Expand Down
160 changes: 160 additions & 0 deletions src/DnsClient/Internal/Base32Hex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System;
using System.Linq;

namespace DnsClient.Internal
{
/// <summary>
/// Base32 encoder with the extended hey alphabet
/// </summary>
/// <remarks>
/// See https://datatracker.ietf.org/doc/html/rfc4648#section-7
/// <![CDATA[
/// Table 4: The "Extended Hex" Base 32 Alphabet
///
/// Value Encoding Value Encoding Value Encoding Value Encoding
/// 0 0 9 9 18 I 27 R
/// 1 1 10 A 19 J 28 S
/// 2 2 11 B 20 K 29 T
/// 3 3 12 C 21 L 30 U
/// 4 4 13 D 22 M 31 V
/// 5 5 14 E 23 N
/// 6 6 15 F 24 O (pad) =
/// 7 7 16 G 25 P
/// 8 8 17 H 26 Q
///
/// ]]>
/// </remarks>
/// <seealso href="https://datatracker.ietf.org/doc/html/rfc4648#section-7">RFC4648</seealso>
public static class Base32Hex
{
/// <summary>
/// Converts the specified string, which encodes binary data as base-32 digits
/// using the extended hex alphabet, to an equivalent 8-bit unsigned integer array.
/// </summary>
/// <param name="input">The string to convert.</param>
/// <returns>An array of 8-bit unsigned integers that is equivalent to <paramref name="input"/>.</returns>
public static byte[] FromBase32HexString(string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}

if (input.Length == 0)
{
return new byte[0];
}

input = input.TrimEnd('=');
var byteCount = input.Length * 5 / 8;
var result = new byte[byteCount];
byte currentByte = 0, bitsRemaining = 8;
var arrayIndex = 0;
foreach (var value in input.Select(CharToValue))
{
int mask;
if (bitsRemaining > 5)
{
mask = value << (bitsRemaining - 5);
currentByte = (byte)(currentByte | mask);
bitsRemaining -= 5;
}
else
{
mask = value >> (5 - bitsRemaining);
currentByte = (byte)(currentByte | mask);
result[arrayIndex++] = currentByte;
unchecked
{
currentByte = (byte)(value << (3 + bitsRemaining));
}
bitsRemaining += 3;
}
}
if (arrayIndex != byteCount)
{
result[arrayIndex] = currentByte;
}

return result;
}

/// <summary>
/// Converts an array of 8-bit unsigned integers to its equivalent string
/// representation that is encoded with base-32 digits using the extended hex alphabet.
/// </summary>
/// <param name="input">An array of 8-bit unsigned integers.</param>
/// <returns>The string representation in base 32 hex of <paramref name="input"/>.</returns>
public static string ToBase32HexString(byte[] input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}

if (input.Length == 0)
{
return string.Empty;
}

var charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
var result = new char[charCount];
byte nextChar = 0, bitsRemaining = 5;
var arrayIndex = 0;
foreach (var b in input)
{
nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
result[arrayIndex++] = ValueToChar(nextChar);
if (bitsRemaining < 4)
{
nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
result[arrayIndex++] = ValueToChar(nextChar);
bitsRemaining += 5;
}
bitsRemaining -= 3;
nextChar = (byte)((b << bitsRemaining) & 31);
}
if (arrayIndex == charCount)
{
return new string(result);
}

result[arrayIndex++] = ValueToChar(nextChar);
while (arrayIndex != charCount)
{
result[arrayIndex++] = '=';
}

return new string(result);
}

private static int CharToValue(char c)
{
var value = c;
if (value <= 58 && value >= 48)
{
return value - 48;
}
if (value <= 86 && value >= 65)
{
return value - 55;
}

throw new ArgumentException("Character is not a Base32 character.", nameof(c));
}

private static char ValueToChar(byte b)
{
if (b < 10)
{
return (char)(b + 48);
}
if (b <= 32)
{
return (char)(b + 55);
}

throw new ArgumentException("Byte is not a value Base32 value.", nameof(b));
}
}
}
Loading

0 comments on commit 7239fe4

Please sign in to comment.