├── Helpers.cs ├── README.md ├── LICENSE ├── StunClientTcp.cs ├── StunClientUdp.cs ├── Attributes ├── StunSoftware.cs ├── StunMappedAddress.cs └── StunXorMappedAddress.cs ├── StunMessage.cs ├── StunTest.cs ├── StunHeader.cs └── StunAttribute.cs /Helpers.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace IceColdMirror { 4 | public static class Helpers { 5 | public static async Task AwaitWithTimeout(this Task task, int timeoutMs) { 6 | await Task.WhenAny(task, Task.Delay(timeoutMs)); 7 | 8 | if (!task.IsCompleted) 9 | throw new System.Exception("Task timedout"); 10 | 11 | return await task; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Very simple STUN client 2 | 3 | [![forthebadge](https://forthebadge.com/images/badges/0-percent-optimized.svg)](https://forthebadge.com) 4 | [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com) 5 | 6 | As the name suggests, this is a very simple and **unfinished** STUN client written in C# and based on [RFC8489](https://tools.ietf.org/html/rfc8489) that can run over TCP or UDP. 7 | 8 | This was made as a proof of concept with the intent to add NAT punching to [Mirror networking](https://mirror-networking.com/) using the STUN protocol. 9 | 10 | It only implements the `XOR-MAPPED-ADDRESS`, `MAPPED-ADDRESS`, and `SOFTWARE` attributes, these are enough to request an IP endpoint to a public STUN server. 11 | 12 | # How to use 13 | 14 | _Please don't_ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Samuel Schultze 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /StunClientTcp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Threading.Tasks; 5 | 6 | namespace IceColdMirror.Stun { 7 | 8 | public class StunClientTcp { 9 | 10 | public async Task SendRequest(StunMessage request, string stunServer) { 11 | return await this.SendRequest(request, new Uri(stunServer)); 12 | } 13 | 14 | public async Task SendRequest(StunMessage request, Uri stunServer) { 15 | if (stunServer.Scheme == "stuns") 16 | throw new NotImplementedException("STUN secure is not supported"); 17 | 18 | if (stunServer.Scheme != "stun") 19 | throw new ArgumentException("URI must have stun scheme", nameof(stunServer)); 20 | 21 | using(var tcp = new TcpClient(new IPEndPoint(IPAddress.Any, 0))) { 22 | var requestBytes = request.Serialize(); 23 | await tcp.ConnectAsync(stunServer.Host, stunServer.Port); 24 | await tcp.GetStream().WriteAsync(requestBytes, 0, requestBytes.Length); 25 | var stream = tcp.GetStream(); 26 | return new StunMessage(stream); 27 | } 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /StunClientUdp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Sockets; 4 | using System.Threading.Tasks; 5 | 6 | namespace IceColdMirror.Stun { 7 | 8 | public class StunClientUdp { 9 | 10 | public async Task SendRequest(StunMessage request, string stunServer) { 11 | return await this.SendRequest(request, new Uri(stunServer)); 12 | } 13 | 14 | public async Task SendRequest(StunMessage request, Uri stunServer) { 15 | if (stunServer.Scheme == "stuns") 16 | throw new NotImplementedException("STUN secure is not supported"); 17 | 18 | if (stunServer.Scheme != "stun") 19 | throw new ArgumentException("URI must have stun scheme", nameof(stunServer)); 20 | 21 | using(var udp = new UdpClient(stunServer.Host, stunServer.Port)) { 22 | var requestBytes = request.Serialize(); 23 | var byteCount = await udp.SendAsync(requestBytes, requestBytes.Length); 24 | var result = await udp.ReceiveAsync(); 25 | 26 | using(var stream = new MemoryStream(result.Buffer)) { 27 | return new StunMessage(stream); 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Attributes/StunSoftware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text; 4 | 5 | namespace IceColdMirror.Stun { 6 | /// 7 | /// The SOFTWARE attribute contains a textual description of the software 8 | /// being used by the agent sending the message. It is used by clients 9 | /// and servers. Its value SHOULD include manufacturer and version 10 | /// number. The attribute has no impact on operation of the protocol and 11 | /// serves only as a tool for diagnostic and debugging purposes. The 12 | /// value of SOFTWARE is variable length. It MUST be a UTF-8-encoded 13 | /// [RFC3629] sequence of fewer than 128 characters (which can be as long 14 | /// as 509 when encoding them and as long as 763 bytes when decoding 15 | /// them). 16 | /// 17 | /// https://tools.ietf.org/html/rfc8489#section-14.14 18 | /// 19 | public static class StunAttributeSoftware { 20 | 21 | public static string GetSoftware(this StunAttribute attribute) { 22 | return Encoding.UTF8.GetString(attribute.Variable); 23 | } 24 | 25 | public static void SetSoftware(this StunAttribute attribute, string info) { 26 | if (info.Length >= 128) 27 | throw new ArgumentException("String must be less than 128 characteres", nameof(info)); 28 | 29 | var bytes = Encoding.UTF8.GetBytes(info); 30 | 31 | if (bytes.Length >= 763) 32 | throw new ArgumentException("String must be less than 763 bytes", nameof(info)); 33 | 34 | attribute.Variable = bytes; 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StunMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using UnityEngine; 6 | 7 | namespace IceColdMirror.Stun { 8 | public class StunMessage { 9 | 10 | public StunMessageHeader header = new StunMessageHeader(); 11 | public List attributes = new List(); 12 | 13 | public StunMessage() { 14 | this.header = new StunMessageHeader(); 15 | } 16 | 17 | public StunMessage(Stream stream) { 18 | this.header = new StunMessageHeader(stream); 19 | 20 | var pos = 0; 21 | while (pos < header.Length) { 22 | var attr = new StunAttribute(stream, this); 23 | pos += attr.AttrbiuteLength; 24 | this.attributes.Add(attr); 25 | } 26 | } 27 | 28 | public byte[] Serialize() { 29 | var serializedAttributes = this.attributes.Select(a => a.Serialize()); 30 | 31 | this.header.Length = (ushort)serializedAttributes.Sum(a => a.Length); 32 | 33 | var message = new byte[this.header.Length + 20]; 34 | var curIndex = 20; 35 | 36 | foreach (var attr in serializedAttributes) { 37 | Array.Copy(attr, 0, message, curIndex, attr.Length); 38 | curIndex += attr.Length; 39 | } 40 | 41 | var header = this.header.Serialize(); 42 | Array.Copy(header, 0, message, 0, 20); 43 | return message; 44 | } 45 | 46 | public override string ToString() => $"{this.header}\n{string.Join("\n", this.attributes.Select(a=>a.ToString()))}"; 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Attributes/StunMappedAddress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using UnityEngine; 4 | 5 | namespace IceColdMirror.Stun { 6 | 7 | public enum AddressFamily : byte { 8 | IPv4 = 0x01, 9 | IPv6 = 0x02, 10 | } 11 | 12 | /// 13 | /// The MAPPED-ADDRESS attribute indicates a reflexive transport address 14 | /// of the client. It consists of an 8-bit address family and a 16-bit 15 | /// port, followed by a fixed-length value representing the IP address. 16 | /// If the address family is IPv4, the address MUST be 32 bits. If the 17 | /// address family is IPv6, the address MUST be 128 bits. All fields 18 | /// must be in network byte order. 19 | /// 20 | /// https://tools.ietf.org/html/rfc8489#section-14.1 21 | /// 22 | public static class StunAttributeMappedAddress { 23 | 24 | public static IPEndPoint GetMappedAddress(this StunAttribute attribute) { 25 | var variable = attribute.Variable; 26 | 27 | if (variable[0] != 0) 28 | Debug.LogWarning($"MAPPED-ADDRESS first byte must be 0x00, got 0x{variable[0].ToString("X2")}"); 29 | 30 | var family = (AddressFamily)variable[1]; 31 | var port = (ushort)((variable[2] << 8) | variable[3]); 32 | var addressSize = variable.Length - sizeof(ushort) * 2; 33 | 34 | if (family == AddressFamily.IPv4 && addressSize != 4) 35 | Debug.LogWarning($"MAPPED-ADDRESS with family {family} needs to have 32 bits, got {addressSize * 8} bits"); 36 | else if (family == AddressFamily.IPv6 && addressSize != 16) 37 | Debug.LogWarning($"MAPPED-ADDRESS with family {family} needs to have 128 bits, got {addressSize * 8} bits"); 38 | 39 | var addressBytes = new byte[addressSize]; 40 | Array.Copy(variable, 4, addressBytes, 0, addressBytes.Length); 41 | var ipAddress = new IPAddress(addressBytes); 42 | 43 | return new IPEndPoint(ipAddress, port); 44 | } 45 | 46 | public static void SetMappedAddress(this StunAttribute attribute, IPEndPoint endPoint) { 47 | throw new NotImplementedException(); 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /StunTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using UnityEngine; 5 | 6 | namespace IceColdMirror.Stun { 7 | public class StunTest : MonoBehaviour { 8 | private async void Start() { 9 | var hosts = new string[] { 10 | "stun://localhost:3478", 11 | "stun://127.0.0.1:3478", 12 | 13 | // Google 14 | "stun://stun.l.google.com:19302", 15 | "stun://stun1.l.google.com:19302", 16 | "stun://stun2.l.google.com:19302", 17 | "stun://stun3.l.google.com:19302", 18 | "stun://stun4.l.google.com:19302", 19 | 20 | // Other 21 | "stun://stun.voip.blackberry.com:3478", 22 | "stun://stun.voipgate.com:3478", 23 | "stun://stun.voys.nl:3478", 24 | "stun://stun1.faktortel.com.au:3478", 25 | 26 | // TCP 27 | "stun://stun.sipnet.net:3478", 28 | "stun://stun.sipnet.ru:3478", 29 | "stun://stun.stunprotocol.org:3478", 30 | }; 31 | 32 | var req = new StunMessage(); 33 | 34 | var software = new StunAttribute(StunAttributeType.SOFTWARE, req); 35 | software.SetSoftware("Ice Cold Mirror"); 36 | 37 | req.attributes.Add(software); 38 | 39 | var clientUdp = new StunClientUdp(); 40 | var clientTcp = new StunClientTcp(); 41 | 42 | await Task.WhenAll(hosts.Select(async(host) => { 43 | try { 44 | var res = await clientUdp.SendRequest(req, host).AwaitWithTimeout(1500); 45 | Debug.Log("UDP: " + res); 46 | var indication = res.attributes.First(a => a.Type == StunAttributeType.XOR_MAPPED_ADDRESS).GetXorMappedAddress(); 47 | Debug.LogWarning($"UDP: {host} STUN indication is {indication}"); 48 | } catch (Exception e) { 49 | Debug.LogException(new Exception($"UDP: STUN host \"{host}\" failed", e)); 50 | } 51 | }).ToArray()); 52 | 53 | await Task.WhenAll(hosts.Select(async(host) => { 54 | try { 55 | var res = await clientTcp.SendRequest(req, host).AwaitWithTimeout(1500); 56 | Debug.Log("TCP: " + res); 57 | var indication = res.attributes.First(a => a.Type == StunAttributeType.XOR_MAPPED_ADDRESS).GetXorMappedAddress(); 58 | Debug.LogWarning($"TCP: {host} STUN indication is {indication}"); 59 | } catch (Exception e) { 60 | Debug.LogException(new Exception($"TCP: STUN host \"{host}\" failed", e)); 61 | } 62 | }).ToArray()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Attributes/StunXorMappedAddress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using UnityEngine; 4 | 5 | namespace IceColdMirror.Stun { 6 | 7 | /// 8 | /// The XOR-MAPPED-ADDRESS attribute is identical to the MAPPED-ADDRESS 9 | /// attribute, except that the reflexive transport address is obfuscated 10 | /// through the XOR function. 11 | /// 12 | /// https://tools.ietf.org/html/rfc8489#section-14.2 13 | /// 14 | public static class StunAttributeXorMappedAddress { 15 | 16 | private static readonly byte[] magicCookieBytes = new byte[] { 17 | (byte)((StunMessageHeader.MAGIC_COOKIE >> 24) & 0xFF), 18 | (byte)((StunMessageHeader.MAGIC_COOKIE >> 16) & 0xFF), 19 | (byte)((StunMessageHeader.MAGIC_COOKIE >> 8) & 0xFF), 20 | (byte)((StunMessageHeader.MAGIC_COOKIE >> 0) & 0xFF), 21 | }; 22 | 23 | public static IPEndPoint GetXorMappedAddress(this StunAttribute attribute) { 24 | var variable = attribute.Variable; 25 | 26 | if (variable[0] != 0) 27 | Debug.LogWarning($"XOR-MAPPED-ADDRESS first byte must be 0x00, got 0x{variable[0].ToString("X2")}"); 28 | 29 | var family = (AddressFamily)variable[1]; 30 | var xPort = (ushort)((variable[2] << 8) | variable[3]); 31 | // xor port with 16 most significant bit of magic cookie 32 | var port = (ushort)(xPort ^ ((magicCookieBytes[0] << 8) | magicCookieBytes[1])); 33 | 34 | var addressSize = variable.Length - sizeof(ushort) * 2; 35 | 36 | if (family == AddressFamily.IPv4 && addressSize != 4) 37 | Debug.LogWarning($"XOR-MAPPED-ADDRESS with family {family} needs to have 32 bits, got {addressSize * 8} bits"); 38 | else if (family == AddressFamily.IPv6 && addressSize != 16) 39 | Debug.LogWarning($"XOR-MAPPED-ADDRESS with family {family} needs to have 128 bits, got {addressSize * 8} bits"); 40 | 41 | var addressBytes = new byte[addressSize]; 42 | Array.Copy(variable, 4, addressBytes, 0, addressBytes.Length); 43 | 44 | // xor each address byte with the magic cookie byte 45 | for (int i = 0; i < 4; i++) 46 | addressBytes[i] ^= magicCookieBytes[i]; 47 | 48 | if (family == AddressFamily.IPv6) { 49 | var header = attribute.Owner.header; 50 | // getting the transaction id generates GC, only call when ipv6 51 | var transactionID = header.TransactionId; 52 | 53 | // xor each byte with the concatenation of magic cookie and transaction id 54 | for (int i = 0; i < transactionID.Length; i++) 55 | addressBytes[i + 4] ^= transactionID[i]; 56 | } 57 | 58 | var ipAddress = new IPAddress(addressBytes); 59 | return new IPEndPoint(ipAddress, port); 60 | } 61 | 62 | public static void SetXorMappedAddress(this StunAttribute attribute, IPEndPoint endPoint) { 63 | throw new NotImplementedException(); 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /StunHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | 5 | namespace IceColdMirror.Stun { 6 | 7 | public enum StunMessageClass : ushort { 8 | Request = 0b00, 9 | Indication = 0b01, 10 | Success = 0b10, 11 | Error = 0b11, 12 | } 13 | 14 | public enum StunMessageMethod : ushort { 15 | Binding = 0x001, 16 | } 17 | 18 | public class StunMessageHeader { 19 | 20 | public const uint MAGIC_COOKIE = 0x2112A442; 21 | 22 | private readonly byte[] data = new byte[20]; 23 | 24 | public ushort Type { 25 | get => (ushort)((data[0] << 8) | data[1]); 26 | set { 27 | this.data[0] = (byte)((value >> 8) & 0xFF); 28 | this.data[1] = (byte)((value >> 0) & 0xFF); 29 | } 30 | } 31 | 32 | public ushort Length { 33 | get => (ushort)((data[2] << 8) | data[3]); 34 | set { 35 | this.data[2] = (byte)((value >> 8) & 0xFF); 36 | this.data[3] = (byte)((value >> 0) & 0xFF); 37 | } 38 | } 39 | 40 | public StunMessageClass Class { 41 | get => (StunMessageClass)( 42 | (this.Type & 0x0100) >> 7 | 43 | (this.Type & 0x0010) >> 4 44 | ); 45 | set => this.Type = (ushort)((ushort)value | (ushort)this.Method); 46 | } 47 | 48 | public StunMessageMethod Method { 49 | get => (StunMessageMethod)( 50 | (this.Type & 0x3E00) >> 2 | 51 | (this.Type & 0x00E0) >> 1 | 52 | (this.Type & 0x000F) 53 | ); 54 | set => this.Type = (ushort)((ushort)value | (ushort)this.Class); 55 | } 56 | 57 | public string TransactionIdBase64 { 58 | get => Convert.ToBase64String(this.data, 8, 96 / 8); 59 | } 60 | 61 | public byte[] TransactionId { 62 | get { 63 | var arr = new byte[96 / 8]; 64 | Array.Copy(this.data, 8, arr, 0, arr.Length); 65 | return arr; 66 | } 67 | } 68 | 69 | public StunMessageHeader() { 70 | this.data[4] = (byte)((MAGIC_COOKIE >> 24) & 0xFF); 71 | this.data[5] = (byte)((MAGIC_COOKIE >> 16) & 0xFF); 72 | this.data[6] = (byte)((MAGIC_COOKIE >> 8) & 0xFF); 73 | this.data[7] = (byte)((MAGIC_COOKIE >> 0) & 0xFF); 74 | 75 | this.Class = StunMessageClass.Request; 76 | this.Method = StunMessageMethod.Binding; 77 | 78 | var rng = new RNGCryptoServiceProvider(); 79 | rng.GetBytes(this.data, 8, 12); // random transaction id 80 | } 81 | 82 | public StunMessageHeader(Stream stream) { 83 | stream.Read(this.data, 0, 20); 84 | 85 | if ((Type & 0xC000) != 0) 86 | throw new ArgumentException("Header must start with two padding zeroes", nameof(stream)); 87 | 88 | var magicCookie = (this.data[4] << 24) | (this.data[5] << 16) | (this.data[6] << 8) | this.data[7]; 89 | 90 | if (magicCookie != MAGIC_COOKIE) 91 | throw new ArgumentException($"Magic cookie doesn't match, expected 0x{MAGIC_COOKIE.ToString("X4")} got 0x{magicCookie.ToString("X4")}", nameof(stream)); 92 | } 93 | 94 | public byte[] Serialize() => this.data; 95 | 96 | public override string ToString() { 97 | return $"StunHeader of type {Type.ToString("X4")}, class {Class}, method {Method}, and {Length} bytes of message"; 98 | } 99 | 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /StunAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace IceColdMirror.Stun { 7 | 8 | public enum StunAttributeType : ushort { 9 | /* Comprehension-required range (0x0000-0x7FFF): */ 10 | MAPPED_ADDRESS = 0x0001, 11 | RESPONSE_ADDRESS = 0x0002, // Reserved [RFC5389] 12 | CHANGE_REQUEST = 0x0003, // Reserved [RFC5389] 13 | SOURCE_ADDRESS = 0x0004, // Reserved [RFC5389] 14 | CHANGED_ADDRESS = 0x0005, // Reserved [RFC5389] 15 | USERNAME = 0x0006, 16 | PASSWORD = 0x0007, // Reserved [RFC5389] 17 | MESSAGE_INTEGRITY = 0x0008, 18 | ERROR_CODE = 0x0009, 19 | UNKNOWN_ATTRIBUTES = 0x000A, 20 | REFLECTED_FROM = 0x000B, // Reserved [RFC5389] 21 | REALM = 0x0014, 22 | NONCE = 0x0015, 23 | XOR_MAPPED_ADDRESS = 0x0020, 24 | 25 | /* Comprehension-optional range (0x8000-0xFFFF) */ 26 | SOFTWARE = 0x8022, 27 | ALTERNATE_SERVER = 0x8023, 28 | FINGERPRINT = 0x8028, 29 | 30 | // New attributes 31 | /* Comprehension-required range (0x0000-0x7FFF): */ 32 | MESSAGE_INTEGRITY_SHA256 = 0x001C, 33 | PASSWORD_ALGORITHM = 0x001D, 34 | USERHASH = 0x001E, 35 | 36 | /* Comprehension-optional range (0x8000-0xFFFF) */ 37 | PASSWORD_ALGORITHMS = 0x8002, 38 | ALTERNATE_DOMAIN = 0x8003, 39 | } 40 | 41 | /** 42 | * 0 1 2 3 43 | * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 44 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 45 | * | Type | Length | 46 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 47 | * | Value (variable) .... 48 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 49 | * 50 | * https://tools.ietf.org/html/rfc8489#section-14 51 | */ 52 | public class StunAttribute { 53 | 54 | private static readonly Dictionary handlers = new Dictionary() { 55 | { 56 | StunAttributeType.SOFTWARE, 57 | typeof(StunAttributeSoftware) 58 | } 59 | }; 60 | 61 | private byte[] data = new byte[4]; // start with space for type and length 62 | 63 | public StunMessage Owner { get; private set; } 64 | 65 | public StunAttributeType Type { 66 | get => (StunAttributeType)((data[0] << 8) | data[1]); 67 | protected set { 68 | this.data[0] = (byte)(((ushort)value >> 8) & 0xFF); 69 | this.data[1] = (byte)(((ushort)value >> 0) & 0xFF); 70 | } 71 | } 72 | 73 | public ushort Length { 74 | get => (ushort)((data[2] << 8) | data[3]); 75 | private set { 76 | this.data[2] = (byte)(((ushort)value >> 8) & 0xFF); 77 | this.data[3] = (byte)(((ushort)value >> 0) & 0xFF); 78 | } 79 | } 80 | 81 | public ushort AttrbiuteLength { 82 | get => (ushort)this.data.Length; 83 | } 84 | 85 | public byte[] Variable { 86 | get { 87 | var arr = new byte[this.Length]; 88 | Array.Copy(data, 4, arr, 0, arr.Length); 89 | return arr; 90 | } 91 | set { 92 | this.Length = (ushort)value.Length; 93 | // data must be aligned to 32-bit boundaries 94 | var padding = (4 - (this.Length % 4)) % 4; 95 | // resize to acommodate type, length, 96 | // variable and the alignment padding 97 | Array.Resize(ref data, this.Length + padding + 4); 98 | Array.Copy(value, 0, this.data, 4, value.Length); 99 | } 100 | } 101 | 102 | public StunAttribute(Stream stream, StunMessage owner) { 103 | stream.Read(this.data, 0, 4); // type and length 104 | var length = this.Length; 105 | var padding = (4 - (this.Length % 4)) % 4; 106 | var variableLength = this.Length + padding; 107 | Array.Resize(ref data, variableLength + 4); 108 | stream.Read(this.data, 4, variableLength); 109 | 110 | this.Owner = owner; 111 | } 112 | 113 | public StunAttribute(StunAttributeType type, StunMessage owner) { 114 | this.Type = type; 115 | this.Owner = owner; 116 | } 117 | 118 | public byte[] Serialize() => this.data; 119 | 120 | public override string ToString() => $"{Type} {Length} bytes: {string.Join(",",Variable.Select(v=>v.ToString("X2")))}"; 121 | 122 | } 123 | 124 | } 125 | --------------------------------------------------------------------------------