using System.Text; namespace XFESystem.Network; /// Bounded RFC 1035 A/CNAME parser. Compression cycles, truncation and unrelated answers are rejected. public static class DnsMessage { public static string NormalizeName(string value) { string name = value.TrimEnd('.').ToLowerInvariant(); if (name.Length is 0 or > 253 || name.Split('.').Any(label => label.Length is 0 or > 63 || label.Any(c => c is not (>= 'a' and <= 'z') and not (>= '0' and <= '9') and not '-'))) throw new ArgumentException("DNS requires a bounded ASCII host name.", nameof(value)); return name; } public static int WriteQuery(Span output, ushort id, string host) { string name = NormalizeName(host); if (output.Length < name.Length + 18) throw new ArgumentException("DNS output is too small."); output.Clear(); NetworkPackets.Write16(output, 0, id); NetworkPackets.Write16(output, 2, 0x0100); NetworkPackets.Write16(output, 4, 1); int at = 12; foreach (string label in name.Split('.')) { output[at++] = (byte)label.Length; at += Encoding.ASCII.GetBytes(label, output[at..]); } output[at++] = 0; NetworkPackets.Write16(output, at, 1); NetworkPackets.Write16(output, at + 2, 1); return at + 4; } static string Name(ReadOnlySpan packet, ref int offset) { int at = offset, resumed = -1, steps = 0; var name = new StringBuilder(64); while (true) { if (++steps > 128 || at >= packet.Length) throw new InvalidDataException("Invalid DNS name or compression cycle."); byte length = packet[at++]; if ((length & 0xc0) == 0xc0) { if (at >= packet.Length) throw new InvalidDataException("Truncated DNS pointer."); int pointer = ((length & 63) << 8) | packet[at++]; if (pointer >= packet.Length) throw new InvalidDataException("DNS pointer is outside the packet."); if (resumed < 0) resumed = at; at = pointer; continue; } if ((length & 0xc0) != 0 || length > 63 || at + length > packet.Length) throw new InvalidDataException("Invalid DNS label."); if (length == 0) { offset = resumed < 0 ? at : resumed; return name.ToString().ToLowerInvariant(); } if (name.Length != 0) name.Append('.'); for (int i = 0; i < length; i++) { byte c = packet[at++]; if (c is not (>= (byte)'a' and <= (byte)'z') and not (>= (byte)'A' and <= (byte)'Z') and not (>= (byte)'0' and <= (byte)'9') and not (byte)'-') throw new InvalidDataException("Invalid DNS host character."); name.Append((char)c); } if (name.Length > 253) throw new InvalidDataException("DNS host name is too long."); } } public static uint ReadAddress(ReadOnlySpan packet, ushort id, string host) { if (packet.Length < 12 || NetworkPackets.Read16(packet, 0) != id) throw new InvalidDataException("DNS transaction mismatch."); ushort flags = NetworkPackets.Read16(packet, 2); if ((flags & 0xf80f) != 0x8000 || (flags & 0x0200) != 0 || NetworkPackets.Read16(packet, 4) != 1) throw new InvalidDataException("DNS response is an error, truncated, or has an unsupported opcode."); string expected = NormalizeName(host); int at = 12; if (Name(packet, ref at) != expected || at + 4 > packet.Length || NetworkPackets.Read16(packet, at) != 1 || NetworkPackets.Read16(packet, at + 2) != 1) throw new InvalidDataException("DNS question mismatch."); at += 4; int count = NetworkPackets.Read16(packet, 6); if (count > 64) throw new InvalidDataException("Too many DNS answers."); var aliases = new Dictionary(StringComparer.OrdinalIgnoreCase); var addresses = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < count; i++) { string owner = Name(packet, ref at); if (at + 10 > packet.Length) throw new InvalidDataException("Truncated DNS answer."); ushort type = NetworkPackets.Read16(packet, at), family = NetworkPackets.Read16(packet, at + 2), length = NetworkPackets.Read16(packet, at + 8); at += 10; int end = at + length; if (end > packet.Length) throw new InvalidDataException("DNS answer exceeds packet."); if (family == 1 && type == 1 && length == 4) addresses[owner] = NetworkPackets.Read32(packet, at); if (family == 1 && type == 5) { int position = at; string alias = Name(packet, ref position); if (position != end) throw new InvalidDataException("Invalid CNAME length."); aliases[owner] = alias; } at = end; } for (int step = 0; step < 8; step++) { if (addresses.TryGetValue(expected, out uint address) && address != 0 && (address >> 28) < 14) return address; if (!aliases.TryGetValue(expected, out string? next)) break; expected = next; } throw new InvalidDataException("DNS has no A address for the requested host/CNAME chain."); } }