diff --git a/TechnitiumLibrary.Net/Dns/EDnsOptions/EDnsCookieOptionData.cs b/TechnitiumLibrary.Net/Dns/EDnsOptions/EDnsCookieOptionData.cs new file mode 100644 index 00000000..fc4cfce5 --- /dev/null +++ b/TechnitiumLibrary.Net/Dns/EDnsOptions/EDnsCookieOptionData.cs @@ -0,0 +1,174 @@ +/* +Technitium Library +Copyright(C) 2026 Shreyas Zare(shreyas @technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using System; +using System.IO; +using System.Text.Json; + +namespace TechnitiumLibrary.Net.Dns.EDnsOptions +{ + /// + /// RFC 7873 DNS COOKIE EDNS option. + /// Option data: + /// - Client cookie: 8 bytes (MUST) + /// - Server cookie: 0 or 8-32 bytes (MAY) + /// Total option data length: 8 OR 16-40 bytes. + /// + public class EDnsCookieOptionData : EDnsOptionData + { + #region variables + + public const int CLIENT_COOKIE_LENGTH = 8; + public const int SERVER_COOKIE_MAX_LENGTH = 32; + public const int SERVER_COOKIE_MIN_LENGTH = 8; + byte[] _clientCookie; + byte[] _serverCookie; // null means absent (client-cookie-only) + + #endregion + + #region constructor + + public EDnsCookieOptionData(byte[] clientCookie, byte[] serverCookie = null) + { + ArgumentNullException.ThrowIfNull(clientCookie); + + if (clientCookie.Length != CLIENT_COOKIE_LENGTH) + throw new ArgumentException("Client cookie must be 8 bytes.", nameof(clientCookie)); + + if (serverCookie is not null && + (serverCookie.Length < SERVER_COOKIE_MIN_LENGTH || serverCookie.Length > SERVER_COOKIE_MAX_LENGTH)) + throw new ArgumentException("Server cookie must be 8-32 bytes.", nameof(serverCookie)); + + _clientCookie = (byte[])clientCookie.Clone(); + _serverCookie = serverCookie is null ? null : (byte[])serverCookie.Clone(); + } + + /// + /// Parsing ctor. The stream is positioned at OPTION-LENGTH (immediately after OPTION-CODE), + /// because EDnsOption(Stream) already read OPTION-CODE. + /// + public EDnsCookieOptionData(Stream s) + : base(s) + { } + + #endregion + + #region protected + + protected override void ReadOptionData(Stream s) + { + // _length is OPTION-LENGTH (bytes of option data). + if (_length < CLIENT_COOKIE_LENGTH) + throw new InvalidDataException($"Invalid COOKIE option length: {_length} bytes"); + + int serverLen = _length - CLIENT_COOKIE_LENGTH; + + // Valid serverLen: 0 OR 8..32. + if (serverLen != 0 && (serverLen < SERVER_COOKIE_MIN_LENGTH || serverLen > SERVER_COOKIE_MAX_LENGTH)) + throw new InvalidDataException($"Invalid server cookie length: {serverLen} bytes. Valid lengths are exactly 0 bytes, or between {SERVER_COOKIE_MIN_LENGTH} and {SERVER_COOKIE_MAX_LENGTH} bytes."); + + _clientCookie = new byte[CLIENT_COOKIE_LENGTH]; + s.ReadExactly(_clientCookie); + + if (serverLen == 0) + { + _serverCookie = null; + return; + } + + _serverCookie = new byte[serverLen]; + s.ReadExactly(_serverCookie); + } + + protected override void WriteOptionData(Stream s) + { + s.Write(_clientCookie); + + if (_serverCookie is not null) + s.Write(_serverCookie); + } + + #endregion + + #region public + + public bool Equals(EDnsCookieOptionData other) + { + if (other is null) + return false; + + if (!_clientCookie.AsSpan().SequenceEqual(other._clientCookie)) + return false; + + if (_serverCookie is null && other._serverCookie is null) + return true; + + if (_serverCookie is null || other._serverCookie is null) + return false; + + return _serverCookie.AsSpan().SequenceEqual(other._serverCookie); + } + + public override bool Equals(object obj) => Equals(obj as EDnsCookieOptionData); + + public override int GetHashCode() + { + HashCode hash = new(); + + foreach (byte b in _clientCookie) + hash.Add(b); + + if (_serverCookie is not null) + foreach (byte b in _serverCookie) + hash.Add(b); + + return hash.ToHashCode(); + } + + public override void SerializeTo(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WriteString("ClientCookie", Convert.ToHexString(_clientCookie)); + + if (_serverCookie is not null) + writer.WriteString("ServerCookie", Convert.ToHexString(_serverCookie)); + + writer.WriteEndObject(); + } + + public override string ToString() + { + if (_serverCookie is null) + return $"COOKIE client={Convert.ToHexString(_clientCookie)}"; + + return $"COOKIE client={Convert.ToHexString(_clientCookie)} server={Convert.ToHexString(_serverCookie)}"; + } + + #endregion + + #region properties + public ReadOnlySpan ClientCookie => _clientCookie; + public bool HasServerCookie => _serverCookie is not null; + public ReadOnlySpan ServerCookie => _serverCookie is null ? ReadOnlySpan.Empty : _serverCookie; + public override int UncompressedLength => CLIENT_COOKIE_LENGTH + (_serverCookie?.Length ?? 0); + + #endregion + } +} \ No newline at end of file diff --git a/TechnitiumLibrary.Net/Dns/EDnsOptions/EDnsOption.cs b/TechnitiumLibrary.Net/Dns/EDnsOptions/EDnsOption.cs index e22b7d51..1ed9fd02 100644 --- a/TechnitiumLibrary.Net/Dns/EDnsOptions/EDnsOption.cs +++ b/TechnitiumLibrary.Net/Dns/EDnsOptions/EDnsOption.cs @@ -78,6 +78,10 @@ public EDnsOption(Stream s) _data = new EDnsExtendedDnsErrorOptionData(s); break; + case EDnsOptionCode.COOKIE: + _data = new EDnsCookieOptionData(s); + break; + default: _data = new EDnsUnknownOptionData(s); break; @@ -92,6 +96,14 @@ public void WriteTo(Stream s) { DnsDatagram.WriteUInt16NetworkOrder((ushort)_code, s); + // OPTION-LENGTH=0 is valid; represent with null data. + if (_data is null) + { + DnsDatagram.WriteUInt16NetworkOrder(0, s); + return; + } + + // EDnsOptionData.WriteTo writes OPTION-LENGTH + option bytes. _data.WriteTo(s); } @@ -128,12 +140,19 @@ public void SerializeTo(Utf8JsonWriter jsonWriter) { jsonWriter.WriteStartObject(); - jsonWriter.WriteString("Code", _code.ToString()); - jsonWriter.WriteString("Length", _data.Length + " bytes"); - - jsonWriter.WritePropertyName("Data"); - _data.SerializeTo(jsonWriter); + jsonWriter.WriteString(nameof(Code), _code.ToString()); + if (_data is null) + { + jsonWriter.WriteString("Length", "0 bytes"); + jsonWriter.WriteNull(nameof(Data)); + } + else + { + jsonWriter.WriteString("Length", _data.Length + " bytes"); + jsonWriter.WritePropertyName(nameof(Data)); + _data.SerializeTo(jsonWriter); + } jsonWriter.WriteEndObject(); } @@ -147,8 +166,7 @@ public EDnsOptionCode Code public EDnsOptionData Data { get { return _data; } } - public int UncompressedLength - { get { return 2 + 2 + _data.UncompressedLength; } } + public int UncompressedLength => 2 + 2 + (_data?.UncompressedLength ?? 0); #endregion }