Skip to content

Commit a7e804c

Browse files
committed
Issue #29, Phase 3 - DNS Protocol Serialization
1 parent 29da198 commit a7e804c

10 files changed

Lines changed: 598 additions & 42 deletions

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ Gotchas:
4848
- ```dotnet format``` all code before submission
4949
- MIT license headers already present — preserve them.
5050

51+
### Endianness
52+
- **DNS uses network byte order (big-endian)** for all multi-byte values per RFC 1035.
53+
- The codebase supports **both big-endian and little-endian host systems** via the `SwapEndian()` extension methods in `Extensions.cs`.
54+
- `SwapEndian()` checks `BitConverter.IsLittleEndian` and only swaps bytes when necessary.
55+
- Use `.SwapEndian()` when reading/writing multi-byte DNS fields (QueryID, counts, TTL, Type, Class, etc.).
56+
- Semantic aliases `NetworkToHost()` and `HostToNetwork()` are available for clarity.
57+
- **Test coverage**: `dnstest/EndianTests.cs` validates correct byte order on any platform.
58+
5159
## 5. Allowed / Disallowed Work
5260
- ✅ Modify C# source, tests, sample configs, docs within `docs/` and root (`AGENTS.md`, README).
5361
- ✅ Add new tests or scripts that live in-repo (delete temporary tooling before submitting).

Dns/DnsProtocol.cs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
namespace Dns
88
{
99
using System;
10+
using System.Buffers.Binary;
1011
using System.Collections.Generic;
1112
using System.IO;
13+
using System.Runtime.CompilerServices;
1214
using System.Text;
1315

1416
public class DnsProtocol
1517
{
18+
/// <summary>Maximum length for a DNS name (255 bytes per RFC 1035).</summary>
19+
private const int MaxDnsNameLength = 255;
20+
1621
/// <summary>Try to parse a DNS message from a byte array.</summary>
1722
/// <param name="bytes">The buffer containing the DNS message.</param>
1823
/// <param name="dnsMessage">The parsed DNS message if successful.</param>
@@ -37,22 +42,170 @@ public static bool TryParse(byte[] bytes, int length, out DnsMessage dnsMessage)
3742
return true;
3843
}
3944

45+
/// <summary>Read a ushort from the buffer (native endian, caller handles swap if needed).</summary>
46+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4047
public static ushort ReadUshort(byte[] bytes, ref int offset)
4148
{
49+
// NOTE: Preserves original semantics - returns native-endian value.
50+
// Callers that need network byte order must call .SwapEndian().
4251
ushort ret = BitConverter.ToUInt16(bytes, offset);
4352
offset += sizeof(ushort);
4453
return ret;
4554
}
4655

56+
/// <summary>Read a big-endian ushort from a span (already network byte order).</summary>
57+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
58+
public static ushort ReadUshortBigEndian(ReadOnlySpan<byte> bytes, ref int offset)
59+
{
60+
ushort ret = BinaryPrimitives.ReadUInt16BigEndian(bytes.Slice(offset));
61+
offset += sizeof(ushort);
62+
return ret;
63+
}
64+
65+
/// <summary>Read a uint from the buffer (native endian, caller handles swap if needed).</summary>
66+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4767
public static uint ReadUint(byte[] bytes, ref int offset)
4868
{
69+
// NOTE: Preserves original semantics - returns native-endian value.
70+
// Callers that need network byte order must call .SwapEndian().
4971
uint ret = BitConverter.ToUInt32(bytes, offset);
5072
offset += sizeof(uint);
5173
return ret;
5274
}
5375

76+
/// <summary>Read a big-endian uint from a span (already network byte order).</summary>
77+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
78+
public static uint ReadUintBigEndian(ReadOnlySpan<byte> bytes, ref int offset)
79+
{
80+
uint ret = BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(offset));
81+
offset += sizeof(uint);
82+
return ret;
83+
}
5484

85+
/// <summary>
86+
/// Reads a DNS domain name from the byte buffer, handling compression pointers.
87+
/// Optimized to minimize allocations using stackalloc and string.Create.
88+
/// </summary>
89+
/// <param name="bytes">The DNS message buffer.</param>
90+
/// <param name="currentOffset">The current read position, updated after reading.</param>
91+
/// <returns>The domain name as a string (without trailing dot).</returns>
5592
public static string ReadString(byte[] bytes, ref int currentOffset)
93+
{
94+
return ReadStringOptimized(bytes.AsSpan(), ref currentOffset);
95+
}
96+
97+
/// <summary>
98+
/// Reads a DNS domain name from a span, handling compression pointers.
99+
/// Uses stackalloc for intermediate storage to minimize heap allocations.
100+
/// </summary>
101+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
102+
public static string ReadStringOptimized(ReadOnlySpan<byte> bytes, ref int currentOffset)
103+
{
104+
// Stack-allocate buffer for the domain name (max 255 chars per RFC 1035)
105+
Span<char> nameBuffer = stackalloc char[MaxDnsNameLength];
106+
int nameLength = 0;
107+
108+
int compressionOffset = -1;
109+
int readOffset = currentOffset;
110+
HashSet<int> pointerVisitedOffsets = null;
111+
112+
while (true)
113+
{
114+
if (readOffset >= bytes.Length)
115+
{
116+
throw new IndexOutOfRangeException("DNS label offset exceeded buffer length.");
117+
}
118+
119+
int segmentLength = bytes[readOffset];
120+
121+
// Compressed name pointer (top 2 bits = 11)
122+
if ((segmentLength & 0xC0) == 0xC0)
123+
{
124+
if (readOffset + 1 >= bytes.Length)
125+
{
126+
throw new IndexOutOfRangeException("DNS compression pointer exceeds buffer length.");
127+
}
128+
129+
pointerVisitedOffsets ??= new HashSet<int>();
130+
if (!pointerVisitedOffsets.Add(readOffset))
131+
{
132+
throw new InvalidDataException("DNS compression pointer cycle detected.");
133+
}
134+
135+
int pointer = ((segmentLength & 0x3F) << 8) | bytes[readOffset + 1];
136+
if (compressionOffset == -1)
137+
{
138+
// Remember where to resume after following the pointer
139+
compressionOffset = readOffset + 2;
140+
}
141+
142+
if (pointer >= bytes.Length)
143+
{
144+
throw new IndexOutOfRangeException("DNS compression pointer targets invalid offset.");
145+
}
146+
147+
readOffset = pointer;
148+
continue;
149+
}
150+
151+
// Null terminator - end of name
152+
if (segmentLength == 0x00)
153+
{
154+
readOffset++;
155+
break;
156+
}
157+
158+
readOffset++;
159+
160+
if (segmentLength > 63)
161+
{
162+
throw new InvalidDataException("DNS label length exceeds maximum of 63 bytes.");
163+
}
164+
165+
if (readOffset + segmentLength > bytes.Length)
166+
{
167+
throw new IndexOutOfRangeException("DNS label exceeds buffer length.");
168+
}
169+
170+
// Check total name length won't exceed max
171+
int requiredLength = nameLength + segmentLength + (nameLength > 0 ? 1 : 0);
172+
if (requiredLength > MaxDnsNameLength)
173+
{
174+
throw new InvalidDataException("DNS name exceeds maximum length of 255 characters.");
175+
}
176+
177+
// Add separator if not the first label
178+
if (nameLength > 0)
179+
{
180+
nameBuffer[nameLength++] = '.';
181+
}
182+
183+
// Copy label bytes directly to char buffer (ASCII only)
184+
ReadOnlySpan<byte> labelBytes = bytes.Slice(readOffset, segmentLength);
185+
for (int i = 0; i < segmentLength; i++)
186+
{
187+
byte b = labelBytes[i];
188+
if (b > 0x7F)
189+
{
190+
throw new InvalidDataException("DNS label contains non-ASCII characters, which are not allowed per RFC 1035.");
191+
}
192+
nameBuffer[nameLength++] = (char)b;
193+
}
194+
195+
readOffset += segmentLength;
196+
}
197+
198+
currentOffset = compressionOffset == -1 ? readOffset : compressionOffset;
199+
200+
// Create string directly from the span - single allocation
201+
return nameLength == 0 ? string.Empty : new string(nameBuffer.Slice(0, nameLength));
202+
}
203+
204+
/// <summary>
205+
/// Legacy ReadString implementation using StringBuilder.
206+
/// Kept for compatibility but ReadStringOptimized is preferred.
207+
/// </summary>
208+
public static string ReadStringLegacy(byte[] bytes, ref int currentOffset)
56209
{
57210
StringBuilder resourceName = new StringBuilder();
58211
int compressionOffset = -1;

Dns/Extensions.cs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Dns
88
{
99
using System;
10+
using System.Buffers.Binary;
1011
using System.IO;
1112
using System.Text;
1213

@@ -19,18 +20,64 @@ public static TextWriter CreateWriter(this Stream stream, Encoding encoding = nu
1920
return new StreamWriter(stream, encoding);
2021
}
2122

23+
/// <summary>
24+
/// Converts a ushort between host byte order and network byte order (big-endian).
25+
/// On little-endian systems, this swaps the bytes. On big-endian systems, this is a no-op.
26+
/// </summary>
27+
/// <remarks>
28+
/// DNS protocol uses network byte order (big-endian) for all multi-byte values.
29+
/// This method handles the conversion regardless of host architecture.
30+
/// </remarks>
2231
public static ushort SwapEndian(this ushort val)
2332
{
24-
ushort value = (ushort)((val << 8) | (val >> 8));
25-
return value;
33+
if (BitConverter.IsLittleEndian)
34+
{
35+
return (ushort)((val << 8) | (val >> 8));
36+
}
37+
return val;
2638
}
2739

40+
/// <summary>
41+
/// Converts a uint between host byte order and network byte order (big-endian).
42+
/// On little-endian systems, this swaps the bytes. On big-endian systems, this is a no-op.
43+
/// </summary>
44+
/// <remarks>
45+
/// DNS protocol uses network byte order (big-endian) for all multi-byte values.
46+
/// This method handles the conversion regardless of host architecture.
47+
/// </remarks>
2848
public static uint SwapEndian(this uint val)
2949
{
30-
uint value = (val << 24) | ((val << 8) & 0x00ff0000) | ((val >> 8) & 0x0000ff00) | (val >> 24);
31-
return value;
50+
if (BitConverter.IsLittleEndian)
51+
{
52+
return (val << 24) | ((val << 8) & 0x00ff0000) | ((val >> 8) & 0x0000ff00) | (val >> 24);
53+
}
54+
return val;
3255
}
3356

57+
/// <summary>
58+
/// Converts a ushort from network byte order (big-endian) to host byte order.
59+
/// Equivalent to SwapEndian but semantically clearer for reading operations.
60+
/// </summary>
61+
public static ushort NetworkToHost(this ushort val) => val.SwapEndian();
62+
63+
/// <summary>
64+
/// Converts a uint from network byte order (big-endian) to host byte order.
65+
/// Equivalent to SwapEndian but semantically clearer for reading operations.
66+
/// </summary>
67+
public static uint NetworkToHost(this uint val) => val.SwapEndian();
68+
69+
/// <summary>
70+
/// Converts a ushort from host byte order to network byte order (big-endian).
71+
/// Equivalent to SwapEndian but semantically clearer for writing operations.
72+
/// </summary>
73+
public static ushort HostToNetwork(this ushort val) => val.SwapEndian();
74+
75+
/// <summary>
76+
/// Converts a uint from host byte order to network byte order (big-endian).
77+
/// Equivalent to SwapEndian but semantically clearer for writing operations.
78+
/// </summary>
79+
public static uint HostToNetwork(this uint val) => val.SwapEndian();
80+
3481

3582

3683
public static byte[] GetResourceBytes(this string str, char delimiter = '.')
@@ -105,13 +152,26 @@ public static string IP(long ipLong)
105152
return b.ToString().ToLower();
106153
}
107154

155+
/// <summary>
156+
/// Writes a ushort to stream in little-endian byte order.
157+
/// </summary>
158+
/// <remarks>
159+
/// Note: For DNS protocol, callers should use .SwapEndian().WriteToStream()
160+
/// to write in network byte order (big-endian).
161+
/// </remarks>
108162
public static void WriteToStream(this ushort value, Stream stream)
109163
{
110164
stream.WriteByte((byte)(value & 0xFF));
111165
stream.WriteByte((byte)((value >> 8) & 0xFF));
112166
}
113167

114-
168+
/// <summary>
169+
/// Writes a uint to stream in little-endian byte order.
170+
/// </summary>
171+
/// <remarks>
172+
/// Note: For DNS protocol, callers should use .SwapEndian().WriteToStream()
173+
/// to write in network byte order (big-endian).
174+
/// </remarks>
115175
public static void WriteToStream(this uint value, Stream stream)
116176
{
117177
stream.WriteByte((byte)(value & 0xFF));

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ Two phases of testing was completed.
100100

101101
Much time was spent using Netmon to capture real DNS challenges and verify that the C# DNS server responded appropriately.
102102

103+
### Endianness Support
104+
The DNS protocol uses **network byte order (big-endian)** for all multi-byte values. The codebase is designed to work correctly on both little-endian (x86, x64, ARM) and big-endian systems:
105+
106+
- The `SwapEndian()` extension methods in `Dns/Extensions.cs` conditionally swap bytes based on `BitConverter.IsLittleEndian`.
107+
- Semantic aliases `NetworkToHost()` and `HostToNetwork()` provide clarity when converting DNS wire format.
108+
- Unit tests in `dnstest/EndianTests.cs` validate correct byte order handling.
109+
103110
### DNS-Sec
104111
No effort made to handle or respond to DNS-Sec challenges.
105112

dnsbench/BenchmarkDotNet.Artifacts/results/DnsBench.DnsProtocolBenchmarks-report-github.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ Apple M1 Max, 1 CPU, 10 logical and 10 physical cores
88
99
1010
```
11-
| Method | Mean | Error | StdDev | Gen0 | Allocated |
12-
|----------------------------------- |------------:|----------:|----------:|-------:|----------:|
13-
| &#39;ReadString: Simple (www.msn.com)&#39; | 60.6599 ns | 0.3570 ns | 0.3164 ns | 0.0471 | 296 B |
14-
| &#39;ReadString: Medium (7 labels)&#39; | 165.7388 ns | 0.6849 ns | 0.5719 ns | 0.1261 | 792 B |
15-
| &#39;ReadString: Compressed pointer&#39; | 80.2575 ns | 0.4812 ns | 0.4018 ns | 0.0739 | 464 B |
16-
| ReadUshort | 0.8081 ns | 0.0049 ns | 0.0038 ns | - | - |
17-
| ReadUint | 0.8254 ns | 0.0271 ns | 0.0266 ns | - | - |
11+
| Method | Mean | Error | StdDev | Gen0 | Allocated |
12+
|------------------------------------------------ |------------:|----------:|----------:|-------:|----------:|
13+
| &#39;ReadString: Simple (legacy StringBuilder)&#39; | 60.2319 ns | 0.4076 ns | 0.3813 ns | 0.0471 | 296 B |
14+
| &#39;ReadString: Simple (Phase 3 Span)&#39; | 34.3911 ns | 0.1426 ns | 0.1264 ns | 0.0076 | 48 B |
15+
| &#39;ReadString: Medium (legacy StringBuilder)&#39; | 164.0997 ns | 0.9025 ns | 0.8442 ns | 0.1261 | 792 B |
16+
| &#39;ReadString: Medium (Phase 3 Span)&#39; | 49.9105 ns | 0.2611 ns | 0.2443 ns | 0.0166 | 104 B |
17+
| &#39;ReadString: Compressed (legacy StringBuilder)&#39; | 81.3111 ns | 0.4062 ns | 0.3800 ns | 0.0739 | 464 B |
18+
| &#39;ReadString: Compressed (Phase 3 Span)&#39; | 51.5990 ns | 0.1687 ns | 0.1409 ns | 0.0344 | 216 B |
19+
| ReadUshort | 0.8055 ns | 0.0058 ns | 0.0054 ns | - | - |
20+
| &#39;ReadUshort (BigEndian Span)&#39; | 0.0000 ns | 0.0000 ns | 0.0000 ns | - | - |
21+
| ReadUint | 0.8114 ns | 0.0016 ns | 0.0014 ns | - | - |
22+
| &#39;ReadUint (BigEndian Span)&#39; | 0.0371 ns | 0.0010 ns | 0.0008 ns | - | - |

0 commit comments

Comments
 (0)