77namespace 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 ;
0 commit comments