diff --git a/Benchmarks/HybridProtoNetworkBenchmark/Program.cs b/Benchmarks/HybridProtoNetworkBenchmark/Program.cs index bba7777..7a73ca2 100644 --- a/Benchmarks/HybridProtoNetworkBenchmark/Program.cs +++ b/Benchmarks/HybridProtoNetworkBenchmark/Program.cs @@ -1,6 +1,7 @@ using NetworkLibrary; using NetworkLibrary.Components.Statistics; using NetworkLibrary.Utils; +using ProtoBuf; using Protobuff; using System.Diagnostics; @@ -73,7 +74,7 @@ static void EchoDynamic(Guid arg1, MessageEnvelope arg2) } static void EchoStatic(Guid arg1, MessageEnvelope arg2) { - server.SendAsyncMessage(arg1, fixedMessage); + server.SendAsyncMessage(arg1, arg2); } private static void InitializeClients() @@ -129,18 +130,101 @@ private static void Benchmark() { Console.WriteLine("Press Enter To Benchmark"); Console.ReadLine(); + var large = new LargeDataClass(); sw2.Start(); Parallel.ForEach(clients, client => { for (int i = 0; i < numMessages; i++) { - client.SendAsyncMessage(clientMessage); + client.SendAsyncMessage(clientMessage,large); } }); } + [ProtoContract] + public class LargeDataClass + { + [ProtoMember(1)] + public int Id { get; set; } + + [ProtoMember(2)] + public string Name { get; set; } + + [ProtoMember(3)] + public double Value { get; set; } + + [ProtoMember(4)] + public DateTime Timestamp { get; set; } + + [ProtoMember(5)] + public NestedData Nested { get; set; } + + [ProtoMember(6)] + public List Tags { get; set; } + [ProtoMember(7)] + public Dictionary Metadata { get; set; } + + [ProtoMember(8)] + public bool IsActive { get; set; } + + [ProtoMember(9)] + public byte[] RawData { get; set; } + + [ProtoMember(10)] + public List NestedList { get; set; } + + public LargeDataClass() + { + // Initialize with some non-default values + Id = 1001; + Name = "Example Name"; + Value = 123.45; + Timestamp = DateTime.UtcNow; + Nested = new NestedData + { + NestedId = 42, + Description = "Nested description", + Data = new byte[] { 1, 2, 3, 4 } + }; + Tags = new List { "tag1", "tag2", "tag3" }; + Metadata = new Dictionary + { + { "key1", 1 }, + { "key2", 2 }, + { "key3", 3 } + }; + IsActive = true; + RawData = new byte[] { 10, 20, 30, 40 }; + NestedList = new List + { + new NestedData { NestedId = 101, Description = "First nested", Data = new byte[] { 5, 6, 7 } }, + new NestedData { NestedId = 102, Description = "Second nested", Data = new byte[] { 8, 9, 10 } } + }; + } + } + + [ProtoContract] + public class NestedData + { + [ProtoMember(1)] + public int NestedId { get; set; } + + [ProtoMember(2)] + public string Description { get; set; } + + [ProtoMember(3)] + public byte[] Data { get; set; } + + public NestedData() + { + // Initialize with non-default values + NestedId = 0; + Description = "Default description"; + Data = Array.Empty(); + } + } private static void ShowStatus() { while (Console.ReadLine() != "e") diff --git a/Benchmarks/RelayBenchmark/Program.cs b/Benchmarks/RelayBenchmark/Program.cs index ef52737..9798e60 100644 --- a/Benchmarks/RelayBenchmark/Program.cs +++ b/Benchmarks/RelayBenchmark/Program.cs @@ -185,27 +185,33 @@ private static void RelayTest() { //string ip = "79.52.134.220"; - string ip = "127.0.0.1"; + //string ip = "172.25.3.45"; + //string ip = "35.159.121.192"; + // string ip = "79.33.180.225"; + string ip = "192.168.1.8"; + // string ip = "127.0.0.1"; + MiniLogger.AllLog += Console.WriteLine; - var cert = new X509Certificate2("client.pfx", "greenpass"); - var scert = new X509Certificate2("server.pfx", "greenpass"); + X509Certificate2 cert = null;//new X509Certificate2("client.pfx", "greenpass"); + X509Certificate2 scert = null;//new X509Certificate2("server.pfx", "greenpass"); if (Console.ReadLine() == "e") { var server = new SecureProtoRelayServer(20020, scert); server.StartServer(); - Task.Run(async () => { while (true) { await Task.Delay(5000); server.GetTcpStatistics(out var generalStats, out _); Console.WriteLine(generalStats.ToString()); } }); + Task.Run(async () => { while (true) { await Task.Delay(5000); server.GetTcpStatistics(out var generalStats, out _); Console.WriteLine(generalStats.ToString()); + server.GetUdpStatistics(out var s, out _); Console.WriteLine(s.ToString()); } }); } Console.ReadLine(); //Task.Run(async () => { while (true) { await Task.Delay(10000); server.GetTcpStatistics(out var generalStats, out _); Console.WriteLine(generalStats.ToString()); } }); var clients = new List(); - int numclients = 50; + int numclients = 100; var pending = new Task[numclients]; Task.Run(async () => { while (true) { await Task.Delay(1000); Console.WriteLine(Interlocked.Exchange(ref sumsum, 0).ToString("N0")); } }); - // Parallel.For(0, numclients, (i) => - for (int i = 0; i < numclients; i++) + Parallel.For(0, numclients, (i) => + //for (int i = 0; i < numclients; i++) { var client = new RelayClient(cert,0); @@ -224,7 +230,7 @@ private static void RelayTest() //Thread.Sleep(1000); } - // ); + ); Task.WaitAll(pending); Console.WriteLine("All Connected"); Thread.Sleep(2000); @@ -234,6 +240,7 @@ private static void RelayTest() { if (client.SessionId == Guid.Empty) throw new Exception(); + // client.StartPingService(); // Console.WriteLine("--- -- - | "+client.sessionId+" count: " + client.Peers.Count); foreach (var peer in client.Peers) { @@ -242,9 +249,9 @@ private static void RelayTest() if (peer.Key == Guid.Empty) throw new Exception(); - //var a = client.RequestTcpHolePunchAsync(peer.Key); - //pndg.Add(a); - // var aa = client.RequestHolePunchAsync(peer.Key, 10000, false); + //var a = client.RequestTcpHolePunchAsync(peer.Key); + //pndg.Add(a); + //var aa = client.RequestHolePunchAsync(peer.Key, 10000, false); //pndg.Add(aa); //client.TestHP(peer.Key, 10000, false); // Console.WriteLine(peer.Key+" cnt=> "+ ++cc); @@ -306,6 +313,7 @@ void ClientMsgReceived(RelayClient client, MessageEnvelope reply) void ClientUdpReceived(RelayClient client, MessageEnvelope reply) { + Interlocked.Increment(ref sumsum); // Interlocked.Increment(ref totMsgCl); //for (int i = 0; i < reply.PayloadCount; i++) diff --git a/Benchmarks/SecureProtobuffBenchmark/Program.cs b/Benchmarks/SecureProtobuffBenchmark/Program.cs index 90836b5..d6dd29f 100644 --- a/Benchmarks/SecureProtobuffBenchmark/Program.cs +++ b/Benchmarks/SecureProtobuffBenchmark/Program.cs @@ -1,6 +1,7 @@ using NetworkLibrary; using NetworkLibrary.Components.Statistics; using NetworkLibrary.Utils; +using ProtoBuf; using Protobuff; using System; using System.Collections.Generic; @@ -75,10 +76,95 @@ static void EchoDynamic(Guid arg1, MessageEnvelope arg2) } static void EchoStatic(Guid arg1, MessageEnvelope arg2) { - server.SendAsyncMessage(arg1, fixedMessage); + server.SendAsyncMessage(arg1, arg2); } + + +[ProtoContract] +public class LargeDataClass +{ + [ProtoMember(1)] + public int Id { get; set; } + + [ProtoMember(2)] + public string Name { get; set; } + + [ProtoMember(3)] + public double Value { get; set; } + + [ProtoMember(4)] + public DateTime Timestamp { get; set; } + + [ProtoMember(5)] + public NestedData Nested { get; set; } + + [ProtoMember(6)] + public List Tags { get; set; } + + [ProtoMember(7)] + public Dictionary Metadata { get; set; } + + [ProtoMember(8)] + public bool IsActive { get; set; } + + [ProtoMember(9)] + public byte[] RawData { get; set; } + + [ProtoMember(10)] + public List NestedList { get; set; } + + public LargeDataClass() + { + // Initialize with some non-default values + Id = 1001; + Name = "Example Name"; + Value = 123.45; + Timestamp = DateTime.UtcNow; + Nested = new NestedData + { + NestedId = 42, + Description = "Nested description", + Data = new byte[] { 1, 2, 3, 4 } + }; + Tags = new List { "tag1", "tag2", "tag3" }; + Metadata = new Dictionary + { + { "key1", 1 }, + { "key2", 2 }, + { "key3", 3 } + }; + IsActive = true; + RawData = new byte[] { 10, 20, 30, 40 }; + NestedList = new List + { + new NestedData { NestedId = 101, Description = "First nested", Data = new byte[] { 5, 6, 7 } }, + new NestedData { NestedId = 102, Description = "Second nested", Data = new byte[] { 8, 9, 10 } } + }; + } +} + +[ProtoContract] +public class NestedData +{ + [ProtoMember(1)] + public int NestedId { get; set; } + + [ProtoMember(2)] + public string Description { get; set; } + + [ProtoMember(3)] + public byte[] Data { get; set; } + + public NestedData() + { + // Initialize with non-default values + NestedId = 0; + Description = "Default description"; + Data = Array.Empty(); + } +} - private static void InitializeClients() +private static void InitializeClients() { clientMessage = new MessageEnvelope() @@ -129,8 +215,10 @@ private static void Prepare() InitializeClients(); } } + private static void Benchmark() { + var large = new LargeDataClass(); Console.WriteLine("Press Enter To Benchmark"); Console.ReadLine(); sw2.Start(); @@ -139,7 +227,7 @@ private static void Benchmark() { for (int i = 0; i < numMessages; i++) { - client.SendAsyncMessage(clientMessage); + client.SendAsyncMessage(clientMessage,large); } }); diff --git a/Benchmarks/TcpBenchmark/Program.cs b/Benchmarks/TcpBenchmark/Program.cs index be8e721..4cd87d4 100644 --- a/Benchmarks/TcpBenchmark/Program.cs +++ b/Benchmarks/TcpBenchmark/Program.cs @@ -14,7 +14,7 @@ namespace ConsoleTest internal class Program { - static int port = 20007; + static int port = 20008; static bool runAsServer; static bool isFixedMessage; static int fixedMessageSize; diff --git a/Json/JsonNetwork.csproj b/Json/JsonNetwork.csproj index 20023c2..f9b19cb 100644 --- a/Json/JsonNetwork.csproj +++ b/Json/JsonNetwork.csproj @@ -5,7 +5,7 @@ True Json.Network.Library Json Network Library - 2.0.0 + 2.0.1 ReferenceType Message passing and P2P network library using System.Text.Json Apache-2.0 diff --git a/Json/MessageProtocol/JsonMessageClient.cs b/Json/MessageProtocol/JsonMessageClient.cs index 0951cea..3492bb9 100644 --- a/Json/MessageProtocol/JsonMessageClient.cs +++ b/Json/MessageProtocol/JsonMessageClient.cs @@ -3,7 +3,7 @@ namespace JsonNetwork.MessageProtocol { - internal class JsonMessageClient : GenericMessageClientWrapper + public class JsonMessageClient : GenericMessageClientWrapper { } } diff --git a/Json/MessageProtocol/JsonMessageServer.cs b/Json/MessageProtocol/JsonMessageServer.cs index 9aa24ad..f822042 100644 --- a/Json/MessageProtocol/JsonMessageServer.cs +++ b/Json/MessageProtocol/JsonMessageServer.cs @@ -3,7 +3,7 @@ namespace JsonNetwork.MessageProtocol { - internal class JsonMessageServer : GenericMessageServerWrapper + public class JsonMessageServer : GenericMessageServerWrapper { public JsonMessageServer(int port) : base(port) { diff --git a/Json/MessageProtocol/SecureJsonMessageClient.cs b/Json/MessageProtocol/SecureJsonMessageClient.cs index af23e80..b5365d6 100644 --- a/Json/MessageProtocol/SecureJsonMessageClient.cs +++ b/Json/MessageProtocol/SecureJsonMessageClient.cs @@ -4,7 +4,7 @@ namespace JsonNetwork.MessageProtocol { - internal class SecureJsonMessageClient : GenericSecureMessageClientWrapper + public class SecureJsonMessageClient : GenericSecureMessageClientWrapper { public SecureJsonMessageClient(X509Certificate2 certificate) : base(certificate) { diff --git a/Json/MessageProtocol/SecureJsonMessageServer.cs b/Json/MessageProtocol/SecureJsonMessageServer.cs index 53b5c78..8327833 100644 --- a/Json/MessageProtocol/SecureJsonMessageServer.cs +++ b/Json/MessageProtocol/SecureJsonMessageServer.cs @@ -4,7 +4,7 @@ namespace JsonNetwork.MessageProtocol { - internal class SecureJsonMessageServer : GenericSecureMessageServerWrapper + public class SecureJsonMessageServer : GenericSecureMessageServerWrapper { public SecureJsonMessageServer(int port, X509Certificate2 cerificate) : base(port, cerificate) { diff --git a/Json/P2P/JsonRelayClient.cs b/Json/P2P/JsonRelayClient.cs index 4a4fce5..02ad2a3 100644 --- a/Json/P2P/JsonRelayClient.cs +++ b/Json/P2P/JsonRelayClient.cs @@ -4,7 +4,7 @@ namespace JsonNetwork.P2P { - internal class JsonRelayClient : RelayClientBase + public class JsonRelayClient : RelayClientBase { public JsonRelayClient(X509Certificate2 clientCert) : base(clientCert) { diff --git a/Json/P2P/Room/JsonRoomClient.cs b/Json/P2P/Room/JsonRoomClient.cs index 667db72..869d654 100644 --- a/Json/P2P/Room/JsonRoomClient.cs +++ b/Json/P2P/Room/JsonRoomClient.cs @@ -4,7 +4,7 @@ namespace JsonNetwork.P2P.Room { - internal class JsonRoomClient : SecureLobbyClient + public class JsonRoomClient : SecureLobbyClient { public JsonRoomClient(X509Certificate2 clientCert) : base(clientCert) { diff --git a/Json/Pure/JsonClient.cs b/Json/Pure/JsonClient.cs index b00fe64..0edfb02 100644 --- a/Json/Pure/JsonClient.cs +++ b/Json/Pure/JsonClient.cs @@ -3,7 +3,7 @@ namespace JsonNetwork.Pure { - internal class JsonClient : GenericClient + public class JsonClient : GenericClient { } } diff --git a/Json/Pure/JsonServer.cs b/Json/Pure/JsonServer.cs index 2e6eba8..29db3b6 100644 --- a/Json/Pure/JsonServer.cs +++ b/Json/Pure/JsonServer.cs @@ -3,7 +3,7 @@ namespace JsonNetwork.Pure { - internal class JsonServer : GenericServer + public class JsonServer : GenericServer { public JsonServer(int port, bool writeLenghtPrefix = true) : base(port, writeLenghtPrefix) { diff --git a/Json/Pure/SecureJsonClient.cs b/Json/Pure/SecureJsonClient.cs index 9bcb6fb..9698341 100644 --- a/Json/Pure/SecureJsonClient.cs +++ b/Json/Pure/SecureJsonClient.cs @@ -4,7 +4,7 @@ namespace JsonNetwork.Pure { - internal class SecureJsonClient : GenericSecureClient + public class SecureJsonClient : GenericSecureClient { public SecureJsonClient(X509Certificate2 certificate, bool writeLenghtPrefix = true) : base(certificate, writeLenghtPrefix) { diff --git a/Json/Pure/SecureJsonServer.cs b/Json/Pure/SecureJsonServer.cs index 7e0fbd5..3577d67 100644 --- a/Json/Pure/SecureJsonServer.cs +++ b/Json/Pure/SecureJsonServer.cs @@ -4,7 +4,7 @@ namespace JsonNetwork.Pure { - internal class SecureJsonServer : GenericSecureServer + public class SecureJsonServer : GenericSecureServer { public SecureJsonServer(int port, X509Certificate2 certificate, bool writeLenghtPrefix = true) : base(port, certificate, writeLenghtPrefix) { diff --git a/MessagePack/MessagePackNetwork.csproj b/MessagePack/MessagePackNetwork.csproj index faa46f3..6cf724b 100644 --- a/MessagePack/MessagePackNetwork.csproj +++ b/MessagePack/MessagePackNetwork.csproj @@ -5,7 +5,7 @@ True MessagePack.Network.Library MessagePack Network Library - 2.0.0 + 2.0.1 ReferenceType Message passing and P2P network library using MessagePack Serialization Apache-2.0 diff --git a/MessagePack/MessageProtocol/MessagePackMessageClient.cs b/MessagePack/MessageProtocol/MessagePackMessageClient.cs index 1a62d69..ee90990 100644 --- a/MessagePack/MessageProtocol/MessagePackMessageClient.cs +++ b/MessagePack/MessageProtocol/MessagePackMessageClient.cs @@ -3,7 +3,7 @@ namespace MessagePackNetwork.MessageProtocol { - internal class MessagePackMessageClient:GenericMessageClientWrapper + public class MessagePackMessageClient:GenericMessageClientWrapper { } diff --git a/MessagePack/MessageProtocol/MessagePackMessageServer.cs b/MessagePack/MessageProtocol/MessagePackMessageServer.cs index 1f9ca4e..55bf2fb 100644 --- a/MessagePack/MessageProtocol/MessagePackMessageServer.cs +++ b/MessagePack/MessageProtocol/MessagePackMessageServer.cs @@ -8,7 +8,7 @@ namespace MessagePackNetwork.MessageProtocol { - internal class MessagePackMessageServer : GenericMessageServerWrapper + public class MessagePackMessageServer : GenericMessageServerWrapper { public MessagePackMessageServer(int port) : base(port) { diff --git a/MessagePack/MessageProtocol/SecureMessagePackMessageClient.cs b/MessagePack/MessageProtocol/SecureMessagePackMessageClient.cs index 05766d6..057bd1a 100644 --- a/MessagePack/MessageProtocol/SecureMessagePackMessageClient.cs +++ b/MessagePack/MessageProtocol/SecureMessagePackMessageClient.cs @@ -9,7 +9,7 @@ namespace MessagePackNetwork.MessageProtocol { - internal class SecureMessagePackMessageClient : GenericSecureMessageClientWrapper + public class SecureMessagePackMessageClient : GenericSecureMessageClientWrapper { public SecureMessagePackMessageClient(X509Certificate2 certificate) : base(certificate) { diff --git a/MessagePack/MessageProtocol/SecureMessagePackMessageServer.cs b/MessagePack/MessageProtocol/SecureMessagePackMessageServer.cs index 7f48b08..7ebac6e 100644 --- a/MessagePack/MessageProtocol/SecureMessagePackMessageServer.cs +++ b/MessagePack/MessageProtocol/SecureMessagePackMessageServer.cs @@ -10,7 +10,7 @@ namespace MessagePackNetwork.MessageProtocol { - internal class SecureMessagePackMessageServer : GenericSecureMessageServerWrapper + public class SecureMessagePackMessageServer : GenericSecureMessageServerWrapper { public SecureMessagePackMessageServer(int port, X509Certificate2 cerificate) : base(port, cerificate) { diff --git a/MessagePack/P2P/MessagePackRelayClient.cs b/MessagePack/P2P/MessagePackRelayClient.cs index a3f3305..63e1103 100644 --- a/MessagePack/P2P/MessagePackRelayClient.cs +++ b/MessagePack/P2P/MessagePackRelayClient.cs @@ -8,7 +8,7 @@ namespace MessagePackNetwork.P2P { - internal class MessagePackRelayClient : RelayClientBase + public class MessagePackRelayClient : RelayClientBase { public MessagePackRelayClient(X509Certificate2 clientCert) : base(clientCert) { diff --git a/MessagePack/P2P/Room/MessagePackRoomClient.cs b/MessagePack/P2P/Room/MessagePackRoomClient.cs index bae2b79..3c2b26d 100644 --- a/MessagePack/P2P/Room/MessagePackRoomClient.cs +++ b/MessagePack/P2P/Room/MessagePackRoomClient.cs @@ -7,7 +7,7 @@ namespace MessagePackNetwork.P2P.Lobby { - internal class MessagePackRoomClient : SecureLobbyClient + public class MessagePackRoomClient : SecureLobbyClient { public MessagePackRoomClient(X509Certificate2 clientCert) : base(clientCert) { diff --git a/MessagePack/Pure/MessagePackClient.cs b/MessagePack/Pure/MessagePackClient.cs index 32c999d..031c1de 100644 --- a/MessagePack/Pure/MessagePackClient.cs +++ b/MessagePack/Pure/MessagePackClient.cs @@ -8,7 +8,7 @@ namespace MessagePackNetwork.Pure { - internal class MessagePackClient:GenericClient + public class MessagePackClient:GenericClient { } } diff --git a/MessagePack/Pure/MessagePackServer.cs b/MessagePack/Pure/MessagePackServer.cs index 22ef10b..70a4a93 100644 --- a/MessagePack/Pure/MessagePackServer.cs +++ b/MessagePack/Pure/MessagePackServer.cs @@ -9,7 +9,7 @@ namespace MessagePackNetwork.Pure { - internal class MessagePackServer : GenericServer + public class MessagePackServer : GenericServer { public MessagePackServer(int port, bool writeLenghtPrefix = true) : base(port, writeLenghtPrefix) { diff --git a/MessagePack/Pure/SecureMessagePackClient.cs b/MessagePack/Pure/SecureMessagePackClient.cs index 13f8e6e..9c932bc 100644 --- a/MessagePack/Pure/SecureMessagePackClient.cs +++ b/MessagePack/Pure/SecureMessagePackClient.cs @@ -9,7 +9,7 @@ namespace MessagePackNetwork.Pure { - internal class SecureMessagePackClient : GenericSecureClient + public class SecureMessagePackClient : GenericSecureClient { public SecureMessagePackClient(X509Certificate2 certificate, bool writeLenghtPrefix = true) : base(certificate, writeLenghtPrefix) { diff --git a/MessagePack/Pure/SecureMessagePackServer.cs b/MessagePack/Pure/SecureMessagePackServer.cs index d75ed58..55cc737 100644 --- a/MessagePack/Pure/SecureMessagePackServer.cs +++ b/MessagePack/Pure/SecureMessagePackServer.cs @@ -9,7 +9,7 @@ namespace MessagePackNetwork.Pure { - internal class SecureMessagePackServer : GenericSecureServer + public class SecureMessagePackServer : GenericSecureServer { public SecureMessagePackServer(int port, X509Certificate2 certificate, bool writeLenghtPrefix = true) : base(port, certificate, writeLenghtPrefix) { diff --git a/NetSerializer/MessageProtocol/NetSerialiserMessageClient.cs b/NetSerializer/MessageProtocol/NetSerialiserMessageClient.cs index b5e48d8..2783015 100644 --- a/NetSerializer/MessageProtocol/NetSerialiserMessageClient.cs +++ b/NetSerializer/MessageProtocol/NetSerialiserMessageClient.cs @@ -3,7 +3,7 @@ namespace NetSerializerNetwork.MessageProtocol { - internal class NetSerialiserMessageClient : GenericMessageClientWrapper + public class NetSerialiserMessageClient : GenericMessageClientWrapper { } } diff --git a/NetSerializer/MessageProtocol/NetSerialiserMessageServer.cs b/NetSerializer/MessageProtocol/NetSerialiserMessageServer.cs index 677cca9..ff3851d 100644 --- a/NetSerializer/MessageProtocol/NetSerialiserMessageServer.cs +++ b/NetSerializer/MessageProtocol/NetSerialiserMessageServer.cs @@ -3,7 +3,7 @@ namespace NetSerializerNetwork.MessageProtocol { - internal class NetSerialiserMessageServer : GenericMessageServerWrapper + public class NetSerialiserMessageServer : GenericMessageServerWrapper { public NetSerialiserMessageServer(int port) : base(port) { diff --git a/NetSerializer/MessageProtocol/SecureNetSerialiserMessageClient.cs b/NetSerializer/MessageProtocol/SecureNetSerialiserMessageClient.cs index de6a774..d55380f 100644 --- a/NetSerializer/MessageProtocol/SecureNetSerialiserMessageClient.cs +++ b/NetSerializer/MessageProtocol/SecureNetSerialiserMessageClient.cs @@ -4,7 +4,7 @@ namespace NetSerializerNetwork.MessageProtocol { - internal class SecureNetSerialiserMessageClient : GenericSecureMessageClientWrapper + public class SecureNetSerialiserMessageClient : GenericSecureMessageClientWrapper { public SecureNetSerialiserMessageClient(X509Certificate2 certificate) : base(certificate) { diff --git a/NetSerializer/MessageProtocol/SecureNetSerialiserMessageServer.cs b/NetSerializer/MessageProtocol/SecureNetSerialiserMessageServer.cs index 35b8f1d..71b671f 100644 --- a/NetSerializer/MessageProtocol/SecureNetSerialiserMessageServer.cs +++ b/NetSerializer/MessageProtocol/SecureNetSerialiserMessageServer.cs @@ -4,7 +4,7 @@ namespace NetSerializerNetwork.MessageProtocol { - internal class SecureNetSerialiserMessageServer : GenericSecureMessageServerWrapper + public class SecureNetSerialiserMessageServer : GenericSecureMessageServerWrapper { public SecureNetSerialiserMessageServer(int port, X509Certificate2 cerificate) : base(port, cerificate) { diff --git a/NetSerializer/NetSerializerNetwork.csproj b/NetSerializer/NetSerializerNetwork.csproj index c908db1..a281793 100644 --- a/NetSerializer/NetSerializerNetwork.csproj +++ b/NetSerializer/NetSerializerNetwork.csproj @@ -5,7 +5,7 @@ True NetSerializer.Network.Library NetSerializer Network Library - 2.0.0 + 2.0.1 Message passing and P2P network library using .NetSerializer Apache-2.0 https://github.com/ReferenceType/StandardNetworkLibrary diff --git a/NetSerializer/P2P/NetSerialiserRelayClient.cs b/NetSerializer/P2P/NetSerialiserRelayClient.cs index 55417c3..80c86d6 100644 --- a/NetSerializer/P2P/NetSerialiserRelayClient.cs +++ b/NetSerializer/P2P/NetSerialiserRelayClient.cs @@ -4,7 +4,7 @@ namespace NetSerializerNetwork.P2P { - internal class NetSerialiserRelayClient : RelayClientBase + public class NetSerialiserRelayClient : RelayClientBase { public NetSerialiserRelayClient(X509Certificate2 clientCert) : base(clientCert) { diff --git a/NetSerializer/P2P/Room/NetSerialiserRoomClient.cs b/NetSerializer/P2P/Room/NetSerialiserRoomClient.cs index 8b68241..1823876 100644 --- a/NetSerializer/P2P/Room/NetSerialiserRoomClient.cs +++ b/NetSerializer/P2P/Room/NetSerialiserRoomClient.cs @@ -4,7 +4,7 @@ namespace NetSerializerNetwork.P2P.Room { - internal class NetSerialiserRoomClient : SecureLobbyClient + public class NetSerialiserRoomClient : SecureLobbyClient { public NetSerialiserRoomClient(X509Certificate2 clientCert) : base(clientCert) { diff --git a/NetSerializer/Pure/NetSerialiserClient.cs b/NetSerializer/Pure/NetSerialiserClient.cs index 025e2e0..0eb85bc 100644 --- a/NetSerializer/Pure/NetSerialiserClient.cs +++ b/NetSerializer/Pure/NetSerialiserClient.cs @@ -3,7 +3,7 @@ namespace NetSerializerNetwork.Pure { - internal class NetSerialiserClient : GenericClient + public class NetSerialiserClient : GenericClient { } } diff --git a/NetSerializer/Pure/NetSerialiserServer.cs b/NetSerializer/Pure/NetSerialiserServer.cs index 72fd940..90a215f 100644 --- a/NetSerializer/Pure/NetSerialiserServer.cs +++ b/NetSerializer/Pure/NetSerialiserServer.cs @@ -3,7 +3,7 @@ namespace NetSerializerNetwork.Pure { - internal class NetSerialiserServer : GenericServer + public class NetSerialiserServer : GenericServer { public NetSerialiserServer(int port, bool writeLenghtPrefix = true) : base(port, writeLenghtPrefix) { diff --git a/NetSerializer/Pure/SecureNetSerialiserClient.cs b/NetSerializer/Pure/SecureNetSerialiserClient.cs index b78f95e..9648500 100644 --- a/NetSerializer/Pure/SecureNetSerialiserClient.cs +++ b/NetSerializer/Pure/SecureNetSerialiserClient.cs @@ -4,7 +4,7 @@ namespace NetSerializerNetwork.Pure { - internal class SecureNetSerialiserClient : GenericSecureClient + public class SecureNetSerialiserClient : GenericSecureClient { public SecureNetSerialiserClient(X509Certificate2 certificate, bool writeLenghtPrefix = true) : base(certificate, writeLenghtPrefix) { diff --git a/NetSerializer/Pure/SecureNetSerialiserServer.cs b/NetSerializer/Pure/SecureNetSerialiserServer.cs index 1303dd6..c765c59 100644 --- a/NetSerializer/Pure/SecureNetSerialiserServer.cs +++ b/NetSerializer/Pure/SecureNetSerialiserServer.cs @@ -4,7 +4,7 @@ namespace NetSerializerNetwork.Pure { - internal class SecureNetSerialiserServer : GenericSecureServer + public class SecureNetSerialiserServer : GenericSecureServer { public SecureNetSerialiserServer(int port, X509Certificate2 certificate, bool writeLenghtPrefix = true) : base(port, certificate, writeLenghtPrefix) { diff --git a/NetworkLibrary/Components/Crypto/AesManager.cs b/NetworkLibrary/Components/Crypto/AesManager.cs index d6645f8..cca4b29 100644 --- a/NetworkLibrary/Components/Crypto/AesManager.cs +++ b/NetworkLibrary/Components/Crypto/AesManager.cs @@ -63,6 +63,29 @@ private IAesAlgorithm Create() } } + public static IAesAlgorithm Create(AesMode AesMode, byte[] Key, byte[] IV) + { + switch (AesMode) + { + case AesMode.CBCRandomIV: + return new AesCbcRandIVAlgorithm(Key, IV); + case AesMode.GCM: + //return new AesGcmManagedAlgorithm(Key, IV); +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + return new AesGcmAlgorithm(Key, IV); +#endif + return new AesGcmManagedAlgorithm(Key, IV); + case AesMode.CBCCtrIV: + return new AesCbcCtrIVAlgorithm(Key, IV); + case AesMode.CBCCtrIVHMAC: + return new AesCbcHmacCtrAlgorithm(Key, IV); + case AesMode.None: + return new NoEncyption(); + + default: throw new NotImplementedException(); + } + } + public byte[] Decrypt(byte[] bytes) { var alg = GetAlgorithm(); diff --git a/NetworkLibrary/Components/Crypto/Algorithms/AesGcmAlgorithm.cs b/NetworkLibrary/Components/Crypto/Algorithms/AesGcmAlgorithm.cs index 7377948..ac67fca 100644 --- a/NetworkLibrary/Components/Crypto/Algorithms/AesGcmAlgorithm.cs +++ b/NetworkLibrary/Components/Crypto/Algorithms/AesGcmAlgorithm.cs @@ -29,8 +29,13 @@ internal class AesGcmAlgorithm : IAesAlgorithm const int tagSize = 14; public AesGcmAlgorithm(byte[] Key, byte[] IV) { +#if NET8_0_OR_GREATER + aes = new AesGcm(Key,tagSize); + aes2 = new AesGcm(Key,tagSize); +#else aes = new AesGcm(Key); aes2 = new AesGcm(Key); +#endif iv = IV; } @@ -79,28 +84,15 @@ public int EncryptInto(byte[] data, int offset, int count, byte[] output, int ou var non = GenerateNextNonce(out int nonceSize); int encryptedDataLength = nonceSize + tagSize + cipherSize; - Span encryptedData; - unsafe - { - fixed (byte* buffer = &output[outputOffset]) - { - encryptedData = new Span(buffer, encryptedDataLength); - } - } + Span encryptedData = new Span(output, outputOffset, encryptedDataLength); + Buffer.BlockCopy(non, 4, output, outputOffset, nonceSize); var nonce = encryptedData.Slice(0, nonceSize); var tag = encryptedData.Slice(nonceSize + cipherSize, tagSize); var cipherBytes = encryptedData.Slice(nonceSize, cipherSize); - // RandomNumberGenerator.Fill(nonce); - - unsafe - { - fixed (byte* buffer = &data[offset]) - aes.Encrypt(non, new ReadOnlySpan(buffer, count), cipherBytes, tag); - } - + aes.Encrypt(non, new ReadOnlySpan(data,offset, count), cipherBytes, tag); return encryptedDataLength; } public int EncryptInto(byte[] data, int offset, int count, @@ -112,14 +104,9 @@ public int EncryptInto(byte[] data, int offset, int count, int cipherSize = count + count1; int encryptedDataLength = tagSize + nonceSize + cipherSize; - Span encryptedData = new Span(output); - unsafe - { - fixed (byte* buffer = &output[outputOffset]) - { - encryptedData = new Span(buffer, encryptedDataLength); - } - } + Span encryptedData = new Span(output, outputOffset, encryptedDataLength); + + Buffer.BlockCopy(non, 4, output, outputOffset, nonceSize); var nonce = encryptedData.Slice(0, nonceSize); @@ -130,28 +117,16 @@ public int EncryptInto(byte[] data, int offset, int count, Buffer.BlockCopy(data, offset, b, 0, count); Buffer.BlockCopy(data1, offset1, b, count, count1); // Generate secure nonce + aes.Encrypt(non, new ReadOnlySpan(b,0, cipherSize), cipherBytes, tag); - unsafe - { - fixed (byte* buffer = &b[0]) - aes.Encrypt(non, new ReadOnlySpan(buffer, cipherSize), cipherBytes, tag); - - } BufferPool.ReturnBuffer(b); return encryptedDataLength; } } public int DecryptInto(byte[] data, int offset, int count, byte[] output, int outputOffset) { - Span encryptedData; - unsafe - { - fixed (byte* buffer = &data[offset]) - { - encryptedData = new Span(buffer, count); - } - } - + Span encryptedData = new Span(data, offset, count); + int oldOff = offset; var nonce = ParseNonce(data, ref offset, ref count).AsSpan(); int nonceSize = offset - oldOff; @@ -162,13 +137,7 @@ public int DecryptInto(byte[] data, int offset, int count, byte[] output, int ou var tag = encryptedData.Slice(nonceSize + cipherSize, tagSize); var cipherBytes = encryptedData.Slice(nonceSize, cipherSize); - unsafe - { - fixed (byte* buffer = &output[outputOffset]) - { - aes2.Decrypt(nonce, cipherBytes, tag, new Span(buffer, cipherSize)); - } - } + aes2.Decrypt(nonce, cipherBytes, tag, new Span(output,outputOffset, cipherSize)); return cipherSize; } @@ -220,4 +189,4 @@ private byte[] ParseNonce(byte[] source, ref int sourceOffset, ref int sourceCou } #endif -} + } diff --git a/NetworkLibrary/Components/Crypto/DiffieHellman/DiffieHellman.cs b/NetworkLibrary/Components/Crypto/DiffieHellman/DiffieHellman.cs new file mode 100644 index 0000000..a09a924 --- /dev/null +++ b/NetworkLibrary/Components/Crypto/DiffieHellman/DiffieHellman.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; + +namespace NetworkLibrary.Components.Crypto.DiffieHellman +{ + public class DiffieHellman + { + // 2048-bit MODP group from RFC 3526 + private const string PrimeHex = "0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BB" + + "EA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E" + + "9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361" + + "C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA1" + + "8217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956" + + "AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF"; + + private static readonly BigInteger _prime = BigInteger.Parse(PrimeHex, System.Globalization.NumberStyles.HexNumber); + + private static readonly BigInteger Generator = new BigInteger(2); // 2 is also from RFC 3526 + + private readonly BigInteger _privateKey; + private readonly BigInteger _publicKey; + public DiffieHellman() + { + _privateKey = GenerateRandomPrivateKey(); + _publicKey = BigInteger.ModPow(Generator, _privateKey, _prime); + } + + public byte[] GetPublicKey() + { + return _publicKey.ToByteArray(); + } + + public byte[] CalculateSharedSecret(byte[] otherPublicKeyBytes) + { + BigInteger otherPublicKey = new BigInteger(otherPublicKeyBytes); + BigInteger sharedSecret = BigInteger.ModPow(otherPublicKey, _privateKey, _prime); + return sharedSecret.ToByteArray(); + } + + + + private BigInteger GenerateRandomPrivateKey() + { + // Recommended key size for security (at least 256 bits) + int keySize = 256 / 8; + byte[] randomBytes = new byte[keySize + 1]; // Extra byte to ensure positive BigInteger + + using (var rng = RandomNumberGenerator.Create()) + { + do + { + rng.GetBytes(randomBytes); + // Ensure private key is in [1, p-1] + var key = new BigInteger(randomBytes); + if (key > 1 && key < _prime - 1) + return key; + } while (true); + } + } + + } +} diff --git a/NetworkLibrary/Components/Crypto/DiffieHellman/EllipticCurveDH.cs b/NetworkLibrary/Components/Crypto/DiffieHellman/EllipticCurveDH.cs new file mode 100644 index 0000000..a3ab233 --- /dev/null +++ b/NetworkLibrary/Components/Crypto/DiffieHellman/EllipticCurveDH.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace NetworkLibrary.Components.Crypto.DiffieHellman +{ +#if NET6_0_OR_GREATER + public class ECDH : IDisposable + { + private readonly ECDiffieHellman _ecdh; + + public ECDH() + { + _ecdh = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256); + } + + public ECDH(ECCurve curve) + { + _ecdh = ECDiffieHellman.Create(curve); + } + + public byte[] GetPublicKey() + { + return _ecdh.PublicKey.ExportSubjectPublicKeyInfo(); + } + + public byte[] CalculateSharedSecret(byte[] otherPublicKeyBytes) + { + using (ECDiffieHellman otherParty = ECDiffieHellman.Create()) + { + otherParty.ImportSubjectPublicKeyInfo(otherPublicKeyBytes, out _); + return _ecdh.DeriveKeyMaterial(otherParty.PublicKey); + } + } + + public void Dispose() + { + _ecdh.Dispose(); + } + } +#endif +} diff --git a/NetworkLibrary/Components/Crypto/DigitalSignature/PrivateKeySign.cs b/NetworkLibrary/Components/Crypto/DigitalSignature/PrivateKeySign.cs new file mode 100644 index 0000000..b52188c --- /dev/null +++ b/NetworkLibrary/Components/Crypto/DigitalSignature/PrivateKeySign.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace NetworkLibrary.Components.Crypto.DigitalSignature +{ + public class PrivateKeySign + { + HMACSHA256 sha256; + public PrivateKeySign(byte[] key) + { + sha256 = new HMACSHA256(key); + } + + public byte[] Sign(byte[] data) + { + return sha256.ComputeHash(data); + } + + public byte[] Sign(byte[] buffer, int offset, int count) + { + return sha256.ComputeHash(buffer, offset, count); + } + + } +} diff --git a/NetworkLibrary/Components/Crypto/DigitalSignature/PublicKeySign.cs b/NetworkLibrary/Components/Crypto/DigitalSignature/PublicKeySign.cs new file mode 100644 index 0000000..d417422 --- /dev/null +++ b/NetworkLibrary/Components/Crypto/DigitalSignature/PublicKeySign.cs @@ -0,0 +1,102 @@ +using NetworkLibrary.Utils; +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace NetworkLibrary.Components.Crypto.DigitalSignature +{ + internal class PublicKeySign + { + + private static readonly ECCurve curve = ECCurve.NamedCurves.nistP256; + + public static (byte[] privateKey, byte[] publicKey) GenerateKeys() + { + using (var ecdsa = ECDsa.Create(curve)) + { + var parameters = ecdsa.ExportParameters(true); + return ( + privateKey: parameters.D, + publicKey: CombineBytes(parameters.Q.X, parameters.Q.Y) + ); + } + } + + public static byte[] SignData(byte[] data,int offset,int count, byte[] privateKey) + { + using (var ecdsa = ECDsa.Create(curve)) + { + ecdsa.ImportParameters(new ECParameters + { + Curve = curve, + D = privateKey, + Q = DerivePublicKey(privateKey) + }); + return ecdsa.SignData(data,offset,count, HashAlgorithmName.SHA256); + } + } + + public static byte[] SignData(byte[] data, int offset, int count, ECParameters privateKeyParams) + { + using (var ecdsa = ECDsa.Create(curve)) + { + ecdsa.ImportParameters(privateKeyParams); + return ecdsa.SignData(data, offset, count, HashAlgorithmName.SHA256); + } + + } + + + public static bool VerifyData(byte[] data,int offset,int count, byte[] signature, byte[] publicKey) + { + using (var ecdsa = ECDsa.Create(curve)) + { + if (publicKey.Length != 64) + throw new ArgumentException("Public key must be 64 bytes (X+Y concatenated)"); + + ecdsa.ImportParameters(new ECParameters + { + Curve = curve, + Q = new ECPoint + { + X = ByteCopy.ToArray(publicKey, 0, 32),//publicKey[0..32], + Y = ByteCopy.ToArray(publicKey, 32, 32) //publicKey[32..64] + } + }); + return ecdsa.VerifyData(data,offset,count, signature, HashAlgorithmName.SHA256); + } + } + + public static bool VerifyData(byte[] data, int offset, int count, byte[] signature, ECParameters publicKeyParams) + { + using (var ecdsa = ECDsa.Create(curve)) + { + ecdsa.ImportParameters(publicKeyParams); + return ecdsa.VerifyData(data, offset, count, signature, HashAlgorithmName.SHA256); + } + } + + private static ECPoint DerivePublicKey(byte[] privateKey) + { + using (var ecdsa = ECDsa.Create(curve)) + { + ecdsa.ImportParameters(new ECParameters + { + Curve = curve, + D = privateKey + }); + return ecdsa.ExportParameters(false).Q; + } + } + + private static byte[] CombineBytes(byte[] first, byte[] second) + { + var result = new byte[first.Length + second.Length]; + Buffer.BlockCopy(first, 0, result, 0, first.Length); + Buffer.BlockCopy(second, 0, result, first.Length, second.Length); + return result; + } + + } +} diff --git a/NetworkLibrary/Components/Crypto/KeyDerivation/HKDF.cs b/NetworkLibrary/Components/Crypto/KeyDerivation/HKDF.cs new file mode 100644 index 0000000..88011a7 --- /dev/null +++ b/NetworkLibrary/Components/Crypto/KeyDerivation/HKDF.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace NetworkLibrary.Components.Crypto.KeyDerivation +{ + public class HKDFLite + { + // HKDF implementation (RFC 5869) + public static byte[] DeriveKey(string source, byte[] salt = null, byte[] info = null, int outputLength = 32) + { + if (string.IsNullOrEmpty(source)) + { + source = "Saltmaker"; + } + var bytes = Encoding.UTF8.GetBytes(source); + if (salt == null) + { + salt = Encoding.UTF8.GetBytes("AES-GCM-Salt"); + } + + if (info == null) + { + info = Encoding.UTF8.GetBytes("AES-GCM-Info"); + } + + byte[] prk = HkdfExtract(salt, bytes); + return HkdfExpand(prk, info, outputLength); + } + public static byte[] DeriveKey(byte[] sharedSecret, byte[] salt = null, byte[] info = null, int outputLength = 32) + { + if (salt == null) + { + salt = Encoding.UTF8.GetBytes("AES-GCM-Salt"); + } + + if (info == null) + { + info = Encoding.UTF8.GetBytes("AES-GCM-Info"); + } + + byte[] prk = HkdfExtract(salt, sharedSecret); + return HkdfExpand(prk, info, outputLength); + } + + private static byte[] HkdfExtract(byte[] salt, byte[] ikm) + { + using (var hmac = new HMACSHA256(salt)) + { + return hmac.ComputeHash(ikm); + } + } + + private static byte[] HkdfExpand(byte[] prk, byte[] info, int outputLength) + { + using (var hmac = new HMACSHA256(prk)) + { + byte[] result = new byte[outputLength]; + byte[] t = new byte[0]; + byte counter = 1; + int offset = 0; + + while (offset < outputLength) + { + // Concatenate T(i-1) + info + counter + byte[] input = new byte[t.Length + info.Length + 1]; + Array.Copy(t, 0, input, 0, t.Length); + Array.Copy(info, 0, input, t.Length, info.Length); + input[t.Length + info.Length] = counter++; + + // Compute T(i) = HMAC-Hash(PRK, T(i-1) | info | i) + t = hmac.ComputeHash(input); + + // Copy to result + int toCopy = Math.Min(t.Length, outputLength - offset); + Array.Copy(t, 0, result, offset, toCopy); + offset += toCopy; + } + + return result; + } + } + } +} diff --git a/NetworkLibrary/Components/MessageBuffer/EncryptedMessageBuffer.cs b/NetworkLibrary/Components/MessageBuffer/EncryptedMessageBuffer.cs index cf2f815..2e32215 100644 --- a/NetworkLibrary/Components/MessageBuffer/EncryptedMessageBuffer.cs +++ b/NetworkLibrary/Components/MessageBuffer/EncryptedMessageBuffer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Drawing; using System.Threading; namespace NetworkLibrary.Components.MessageBuffer @@ -152,5 +154,29 @@ public void Flush() { flushStream.Clear(); } + + public bool TryEnqueueMessage(List> segments) + { + foreach (var segment in segments) + { + if (currentIndexedMemory < MaxIndexedMemory && !disposedValue) + { + + + writeStream.Reserve(segment.Count + 256); + + int amount = aesAlgorithm.EncryptInto(segment.Array, segment.Offset, segment.Count, writeStream.GetBuffer(), writeStream.Position32 + 4); + + writeStream.WriteInt(amount); + writeStream.Position32 += amount; + + currentIndexedMemory += segment.Count; + } + else return false; + } + TotalMessageDispatched++; + + return true; + } } } diff --git a/NetworkLibrary/Components/MessageBuffer/Interface/IMessageProcessQueue.cs b/NetworkLibrary/Components/MessageBuffer/Interface/IMessageProcessQueue.cs index 26e0153..786501c 100644 --- a/NetworkLibrary/Components/MessageBuffer/Interface/IMessageProcessQueue.cs +++ b/NetworkLibrary/Components/MessageBuffer/Interface/IMessageProcessQueue.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace NetworkLibrary.Components { @@ -17,6 +18,7 @@ public interface IMessageQueue : IDisposable /// /// true if message is enqueued. bool TryEnqueueMessage(byte[] bytes, int offset, int count); + bool TryEnqueueMessage(List> segments); /// /// Flushes the queue if there is anything to flush. diff --git a/NetworkLibrary/Components/MessageBuffer/MessageBuffer.cs b/NetworkLibrary/Components/MessageBuffer/MessageBuffer.cs index c01bcf1..47234e2 100644 --- a/NetworkLibrary/Components/MessageBuffer/MessageBuffer.cs +++ b/NetworkLibrary/Components/MessageBuffer/MessageBuffer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Drawing; using System.Threading; @@ -153,5 +154,30 @@ public void Flush() { flushStream.Clear(); } + + public bool TryEnqueueMessage(List> segments) + { + lock (bufferMtex) + { + foreach (var segment in segments) + { + if (currentIndexedMemory < MaxIndexedMemory && !disposedValue) + { + if (writeLengthPrefix) + { + currentIndexedMemory += 4; + writeStream.WriteInt(segment.Count); + } + + writeStream.Write(segment.Array, segment.Offset, segment.Count); + currentIndexedMemory += segment.Count; + + } + else return false; + } + TotalMessageDispatched++; + } + return true; + } } } diff --git a/NetworkLibrary/Components/MessageBuffer/MessageQueue.cs b/NetworkLibrary/Components/MessageBuffer/MessageQueue.cs index 8a5e0b3..f9b487c 100644 --- a/NetworkLibrary/Components/MessageBuffer/MessageQueue.cs +++ b/NetworkLibrary/Components/MessageBuffer/MessageQueue.cs @@ -1,5 +1,8 @@ using NetworkLibrary.Utils; +using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Drawing; using System.Runtime.CompilerServices; using System.Threading; @@ -30,6 +33,7 @@ public bool TryEnqueueMessage(byte[] bytes) { Interlocked.Add(ref currentIndexedMemory, bytes.Length); SendQueue.Enqueue(bytes); + totalMessageFlushed++; return true; } return false; @@ -52,8 +56,6 @@ public bool TryFlushQueue(ref byte[] buffer, int offset, out int amountWritten) int memcount = 0; while (SendQueue.TryDequeue(out byte[] bytes)) { - totalMessageFlushed++; - memcount += bytes.Length; if (!processor.ProcessMessage(bytes)) { @@ -85,5 +87,24 @@ public void Dispose() } public void Flush() { } + + public bool TryEnqueueMessage(List> segments) + { + foreach (var segment in segments) + { + var array = ByteCopy.ToArray(segment.Array, segment.Offset, segment.Count); + if (Volatile.Read(ref currentIndexedMemory) < MaxIndexedMemory) + { + Interlocked.Add(ref currentIndexedMemory, array.Length); + SendQueue.Enqueue(array); + } + else + { + return false; + } + } + totalMessageFlushed++; + return true; + } } } diff --git a/NetworkLibrary/Components/MessageProcessor/Unmanaged/DelimitedMessageWriter.cs b/NetworkLibrary/Components/MessageProcessor/Unmanaged/DelimitedMessageWriter.cs index 2356fe8..0faa5e2 100644 --- a/NetworkLibrary/Components/MessageProcessor/Unmanaged/DelimitedMessageWriter.cs +++ b/NetworkLibrary/Components/MessageProcessor/Unmanaged/DelimitedMessageWriter.cs @@ -4,7 +4,6 @@ namespace NetworkLibrary.Components.MessageProcessor.Unmanaged { internal sealed class DelimitedMessageWriter - : IMessageProcessor { private byte[] bufferInternal; diff --git a/NetworkLibrary/DistributedP2P/Channels/Components/ChannelFactory.cs b/NetworkLibrary/DistributedP2P/Channels/Components/ChannelFactory.cs new file mode 100644 index 0000000..86a9d3d --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/Components/ChannelFactory.cs @@ -0,0 +1,91 @@ +using NetworkLibrary.DistributedP2P.Client.StateManagement; +using NetworkLibrary.DistributedP2P.Client; +using System; +using System.Collections.Generic; +using System.Text; +using System.Net.Sockets; +using System.Net; +using NetworkLibrary.Components.Crypto.KeyDerivation; +using NetworkLibrary.Components.Crypto; +using NetworkLibrary.TCP.AES; +using NetworkLibrary.DistributedP2P.Components; + +namespace NetworkLibrary.DistributedP2P.Channels.Components +{ + internal class ChannelFactory + { + public static IChannel CreateChannel(ClientSimultaneousTcpHolepunchState TcpHpstate, bool isInitiator, ILogger logger) + { + ChannelInfo info = TcpHpstate.ChannelInfo; + Socket connectedSocket = TcpHpstate.Socket; + byte[] sharedSecret = TcpHpstate.SharedSecret; + IPEndPoint endpoint = TcpHpstate.SuccesfulEndpoint; + ChannelType channelType = TcpHpstate.ChannelInfo.ChannelType; + + return CreateChannel(info, connectedSocket, sharedSecret, endpoint, channelType, isInitiator, logger); + } + + public static IChannel CreateChannel(ClientSequentialTcpHolepunchState TcpHpstate, bool isInitiator, ILogger logger) + { + ChannelInfo info = TcpHpstate.ChannelInfo; + Socket connectedSocket = TcpHpstate.Socket; + byte[] sharedSecret = TcpHpstate.SharedSecret; + IPEndPoint endpoint = TcpHpstate.SuccesfulEndpoint; + ChannelType channelType = TcpHpstate.ChannelInfo.ChannelType; + + return CreateChannel(info, connectedSocket, sharedSecret, endpoint, channelType, isInitiator, logger); + } + + public static IChannel CreateChannel(ClientUdpHolepunchState udpHpstate, bool isInitiator, ILogger logger) + { + ChannelInfo info = udpHpstate.ChannelInfo; + Socket connectedSocket = udpHpstate.Socket; + byte[] sharedSecret = udpHpstate.SharedSecret; + IPEndPoint endpoint = udpHpstate.SuccesfulEndpoint; + ChannelType channelType = udpHpstate.ChannelInfo.ChannelType; + + return CreateChannel(info, connectedSocket, sharedSecret, endpoint, channelType, isInitiator, logger); + } + + public static IChannel CreateChannel(ClientPipeState pipeState, bool isInitiator, ILogger logger) + { + ChannelInfo info = pipeState.ChannelInfo; + Socket connectedSocket = pipeState.ConnectedSocket; + byte[] sharedSecret = pipeState.sharedSecret; + IPEndPoint endpoint = pipeState.SuccesfullEndpoint.ToIpEndpoint(); + ChannelType channelType = pipeState.ChannelInfo.ChannelType; + + return CreateChannel(info, connectedSocket, sharedSecret, endpoint, channelType, isInitiator, logger); + } + + public static IChannel CreateChannel(ChannelInfo info, Socket connectedSocket, byte[] sharedSecret, IPEndPoint endpoint, ChannelType channelType, bool isInitiator, ILogger logger) + { + IChannel channel = null; + + switch (channelType) + { + + case ChannelType.Tcp: + channel = new TcpChannel(info, connectedSocket, logger); + break; + case ChannelType.SecureTcp: + var symetricKey = HKDFLite.DeriveKey(sharedSecret, outputLength: 16); + var algo = AesManager.Create(AesMode.GCM, symetricKey, HKDFLite.DeriveKey(info.ChannelName,outputLength:16) ); + channel = new SecureTcpChannel(algo, info, connectedSocket, isInitiator, logger); + break; + case ChannelType.Udp: + channel = new UdpChannel(connectedSocket, endpoint, info, logger); + + break; + case ChannelType.SecureUdp: + var symetricKey2 = HKDFLite.DeriveKey(sharedSecret, outputLength: 16); + var algo2 = AesManager.Create(AesMode.GCM, symetricKey2, HKDFLite.DeriveKey(info.ChannelName,outputLength: 16)); + channel = new SecureUdpChannel(connectedSocket, endpoint, algo2, info, isInitiator, logger); + + break; + } + + return channel; + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Channels/Components/EphemeralKeyManager.cs b/NetworkLibrary/DistributedP2P/Channels/Components/EphemeralKeyManager.cs new file mode 100644 index 0000000..9efc3e7 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/Components/EphemeralKeyManager.cs @@ -0,0 +1,160 @@ +using NetworkLibrary.Components; +using NetworkLibrary.Components.Crypto; +using NetworkLibrary.Components.Crypto.Algorithms; +using NetworkLibrary.Components.Crypto.DiffieHellman; +using NetworkLibrary.Components.Crypto.KeyDerivation; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.Utils; +using System; +using System.Collections.Concurrent; +using System.Security.Cryptography; + +namespace NetworkLibrary.DistributedP2P.Channels.Components +{ + internal class EphemeralKeyManager + { + public Action SendData; + + DiffieHellman df = new DiffieHellman(); + + internal ConcurrentDictionary keyStore = new ConcurrentDictionary(); + + byte[] innerBuffer = new byte[1024]; + + byte currKeyNumber = 0; + internal byte CurrKeyNumber = 0; + private int keyRotationPeriod; + private bool closed = false; + private byte[] IV = new byte[16]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + Guid timerGuid = Guid.NewGuid(); + + int overallCounter = 0; + public EphemeralKeyManager(IAesAlgorithm initialKey, int keyRotationPeriod = 1000) + { + keyStore[0] = initialKey; + this.keyRotationPeriod = keyRotationPeriod; + TimedKeyExchange(); + } + + private void TimedKeyExchange() + { + if (keyRotationPeriod < 0) + return; + TimerService.RegisterTimer(timerGuid, keyRotationPeriod, () => + { + RequestKeyExchange(); + }); + } + + public void HandleMessage(MessageFlags flag, byte[] buffer, int offset, int count) + { + + if (closed) return; + + switch (flag) + { + case MessageFlags.KeyExchange: + HandleKeyExchangeReq(buffer, offset, count); + break; + + case MessageFlags.KeyExchangeAck: + HandleKeyExchangeAck(buffer, offset, count); + break; + + case MessageFlags.KeyExchangeFin: + HandleFinalize(); + break; + } + } + + + //[Alice] + private void RequestKeyExchange() + { + if (closed) return; + + df = new DiffieHellman(); + byte[] myPublic = df.GetPublicKey(); + SendData?.Invoke(MessageFlags.KeyExchange, myPublic, 0, myPublic.Length); + + } + + //[Bob] + private void HandleKeyExchangeReq(byte[] buffer, int offset, int count) + { + if (closed) return; + + currKeyNumber++; + overallCounter++; + + df = new DiffieHellman(); + var sharedSecret = df.CalculateSharedSecret(ByteCopy.ToArray(buffer, offset, count)); + var privKey = HKDFLite.DeriveKey(sharedSecret, outputLength: 16); + var iv = HKDFLite.DeriveKey("DiffieHellman" + overallCounter.ToString(), outputLength: 16); + + keyStore[currKeyNumber] = AesManager.Create(AesMode.GCM, privKey, iv); + + byte[] myPublic = df.GetPublicKey(); + SendData?.Invoke(MessageFlags.KeyExchangeAck, myPublic, 0, myPublic.Length); + } + + //[Alice] + private void HandleKeyExchangeAck(byte[] buffer, int offset, int count) + { + if (closed) return; + + currKeyNumber++; + overallCounter++; + + var sharedSecret = df.CalculateSharedSecret(ByteCopy.ToArray(buffer, offset, count)); + var privKey = HKDFLite.DeriveKey(sharedSecret, outputLength: 16); + var iv = HKDFLite.DeriveKey("DiffieHellman" + overallCounter.ToString(), outputLength: 16); + + keyStore[currKeyNumber] = AesManager.Create(AesMode.GCM, privKey, iv); ; + + rng.GetBytes(innerBuffer, 0, 32); + SendData?.Invoke(MessageFlags.KeyExchangeFin, innerBuffer, 0, 32); + + CurrKeyNumber = currKeyNumber; + TimedKeyExchange(); + } + + //[Bob] + private void HandleFinalize() + { + // Console.WriteLine("KeyExchanged"); + if (closed) return; + + CurrKeyNumber = currKeyNumber; + } + + + public bool GetAlgorithm(byte keyNum, out IAesAlgorithm algo) + { + return keyStore.TryGetValue(keyNum, out algo); + } + + internal void Close() + { + closed = true; + } + + internal void SetKeyRotationTime(int timeMs) + { + TimerService.CancelTimeout(timerGuid); + if (timeMs < 0) + { + keyRotationPeriod = timeMs; + return; + } + if (timeMs < 1000) + timeMs = 1000; + + keyRotationPeriod = timeMs; + TimedKeyExchange(); + } + + + } +} diff --git a/NetworkLibrary/DistributedP2P/Channels/Components/IChannel.cs b/NetworkLibrary/DistributedP2P/Channels/Components/IChannel.cs new file mode 100644 index 0000000..5fefbfd --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/Components/IChannel.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using NetworkLibrary.DistributedP2P.Client; + +namespace NetworkLibrary.DistributedP2P.Channels.Components +{ + + public interface IChannel + { + event Action OnBytesReceived; + + event Action OnDisconnected; + + /// + /// Information about channel type + /// + ChannelInfo Info { get; } + + /// + /// Starts data receiver. + /// Start must be called after all event subscription is made + /// + void Start(); + + /// + /// Sends buytes to destionation asyncronusly. + /// + /// + /// + /// + void Send(byte[] buffer, int offset, int count); + + /// + /// Pings the channel, -1 means ping failed. + /// + /// + Task Ping(); + + /// + /// Closes the channel and releases resources. + /// + void CloseChannel(); + + + + } +} \ No newline at end of file diff --git a/NetworkLibrary/DistributedP2P/Channels/Components/KeepAlive.cs b/NetworkLibrary/DistributedP2P/Channels/Components/KeepAlive.cs new file mode 100644 index 0000000..5cdc07a --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/Components/KeepAlive.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Channels.Components +{ + + public class KeepAlive + { + public Action SendData; + public Action NotAlive; + + private DateTime lastReceived = DateTime.Now; + private bool stop = false; + private byte[] innerBuff = new byte[32]; + public KeepAlive() + { + StartSendRoutine(); + } + + private async void StartSendRoutine() + { + while (!stop) + { + await Task.Delay(4000); + if (stop) break; + SendKeepAlive(); + + if ((DateTime.Now - lastReceived).TotalMilliseconds > 10000) + { + DisconnectDetected(); + } + } + } + + private void DisconnectDetected() + { + NotAlive?.Invoke(); + } + + public void HandleMessage(MessageFlags flag, byte[] buffer, int offset, int count) + { + HandleKeepAlive(buffer, offset, count); + } + + RandomNumberGenerator r = RandomNumberGenerator.Create(); + private void SendKeepAlive() + { + r.GetBytes(innerBuff, 0, 16); + SendData?.Invoke(MessageFlags.KeepAliveMessage, innerBuff, 0, 16); + } + + private void HandleKeepAlive(byte[] buffer, int offset, int count) + { + lastReceived = DateTime.Now; + } + + internal void Close() + { + stop = true; + NotAlive = null; + SendData = null; + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Channels/Components/MessageFlags.cs b/NetworkLibrary/DistributedP2P/Channels/Components/MessageFlags.cs new file mode 100644 index 0000000..7fe897d --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/Components/MessageFlags.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.Channels.Components +{ + public enum MessageFlags : byte + { + StandardMessage, + JumboMessage, + ReliableMessage, + InternalReliableMessage, + KeepAliveMessage, + HP, + HPAck, + Ping, + Pong, + KeyExchange, + KeyExchangeAck, + KeyExchangeFin, + Kill, + } + + + +} diff --git a/NetworkLibrary/DistributedP2P/Channels/Components/Pinger.cs b/NetworkLibrary/DistributedP2P/Channels/Components/Pinger.cs new file mode 100644 index 0000000..e0f6e24 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/Components/Pinger.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Channels.Components +{ + + public class Pinger + { + + public Action SendData; + byte[] innerBuffer = new byte[16]; + ConcurrentDictionary> dispatched = new ConcurrentDictionary>(); + public void HandleMessage(MessageFlags flag, byte[] buffer, int offset, int count) + { + switch (flag) + { + case MessageFlags.Ping: + HandlePing(buffer, offset, count); + break; + case MessageFlags.Pong: + HandlePong(buffer, offset); + break; + } + } + + //[A] + public async Task Ping() + { + Guid guid = Guid.NewGuid(); + var pong = new TaskCompletionSource(); + int offset = 0; + PrimitiveEncoder.WriteGuid(innerBuffer, ref offset, guid); + dispatched[guid] = pong; + + var sendTime = DateTime.Now; + SendData?.Invoke(MessageFlags.Ping, innerBuffer, 0, 16); + + var result = await Task.WhenAny(pong.Task, Task.Delay(10000)); + if (result == pong.Task) + { + var replyTime = await pong.Task; + return (replyTime - sendTime).TotalMilliseconds; + } + else + { + dispatched.TryRemove(guid, out _); + return -1; + } + } + + //[B] + private void HandlePing(byte[] buffer, int offset, int count) + { + SendData?.Invoke(MessageFlags.Pong, buffer, offset, count); + } + + //[A] + private void HandlePong(byte[] buffer, int offset) + { + var guid = PrimitiveEncoder.ReadGuid(buffer, ref offset); + if (dispatched.TryRemove(guid, out var tcs)) + { + tcs.TrySetResult(DateTime.Now); + } + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Channels/Components/UdpChannelBase.cs b/NetworkLibrary/DistributedP2P/Channels/Components/UdpChannelBase.cs new file mode 100644 index 0000000..494f529 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/Components/UdpChannelBase.cs @@ -0,0 +1,156 @@ +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace NetworkLibrary.DistributedP2P.Channels.Components +{ + internal class UdpChannelBase : IDisposable + { + private Socket udpSocket; + private SocketAsyncEventArgs receiveArgs; + private readonly IPEndPoint associatedEndpoint; + private readonly ILogger logger; + int disposed = 0; + + + public ChannelInfo Info { get; private set; } + + public event Action OnBytesReceived; + public event Action OnDisconnected; + public UdpChannelBase(Socket udpSocket, IPEndPoint receiveEp, ChannelInfo info, ILogger logger) + { + this.udpSocket = udpSocket; + associatedEndpoint = receiveEp; + Info = info; + this.logger = logger; + } + + public void Start() + { + StartReceiver(); + } + + public void Send(byte[] data, int offset, int count) + { + udpSocket.SendTo(data, offset, count, SocketFlags.None, associatedEndpoint); + } + + private void StartReceiver() + { + var buff = BufferPool.RentBuffer(65536); + + receiveArgs = new SocketAsyncEventArgs(); + receiveArgs.SetBuffer(buff, 0, buff.Length); + receiveArgs.Completed += OnReceiveCompleted; + receiveArgs.RemoteEndPoint = associatedEndpoint; + Receive(); + } + + private void Receive() + { + if (!udpSocket.ReceiveFromAsync(receiveArgs)) + { + ThreadPool.UnsafeQueueUserWorkItem((s) => OnReceiveCompleted(null, receiveArgs), null); + } + } + + private void OnReceiveCompleted(object sender, SocketAsyncEventArgs e) + { + while (true) + { + if (e.SocketError != SocketError.Success) + { + HandleSocketError(e.SocketError); + return; + } + + if (e.BytesTransferred > 0) + { + try + { + ProcessReceivedData(e.Buffer, e.Offset, e.BytesTransferred, e.RemoteEndPoint); + } + catch (Exception ex) + { + Log(LogType.Debug,$"{ex.Message}\n{ex.StackTrace}"); + CloseChannel(); + throw; + } + } + else + { + CloseChannel(); + return; + } + + + if (Interlocked.CompareExchange(ref disposed, 0, 0) == 1) + { + return; + } + if (udpSocket.ReceiveFromAsync(receiveArgs)) + { + return; + } + } + + } + + private void ProcessReceivedData(byte[] buffer, int offset, int bytesTransferred, EndPoint remoteEndPoint) + { + OnBytesReceived?.Invoke(buffer, offset, bytesTransferred); + } + + private void HandleSocketError(SocketError error) + { + if (error != SocketError.Shutdown) + Log(LogType.Debug,$"Socket error occurred: {error}"); + + CloseChannel(); + } + + private void Log(LogType type,string err) + { + logger?.Log(type, err); + } + + public virtual void CloseChannel() + { + Interlocked.Exchange(ref OnDisconnected, null)?.Invoke(); + Dispose(); + } + + + public virtual void Dispose() + { + if (Interlocked.CompareExchange(ref disposed, 1, 0) == 0) + { + try + { + if (receiveArgs != null) + { + + BufferPool.ReturnBuffer(receiveArgs.Buffer); + receiveArgs.Dispose(); + receiveArgs = null; + } + + if (udpSocket != null) + { + udpSocket.Shutdown(SocketShutdown.Both); + udpSocket.Close(); + udpSocket.Dispose(); + udpSocket = null; + } + } + catch { } + OnBytesReceived = null; + OnDisconnected = null; + } + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Channels/SecureTcpChannel.cs b/NetworkLibrary/DistributedP2P/Channels/SecureTcpChannel.cs new file mode 100644 index 0000000..685290c --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/SecureTcpChannel.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using NetworkLibrary.Components; +using NetworkLibrary.Components.Crypto; +using NetworkLibrary.Components.Crypto.Algorithms; +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.TCP.AES; +using NetworkLibrary.Utils; + +namespace NetworkLibrary.DistributedP2P.Channels +{ + public class SecureTcpChannel : TcpChannel + { + private readonly bool isInitiator; + private EphemeralKeyManager keyManager; + + private byte[] encBuff; + private byte[] decBuff; + /// + /// -1 means never rotate keys + /// + public int KeyRotationPeriodMs { get; private set; } = 1000; + + public SecureTcpChannel(IAesAlgorithm algo,ChannelInfo info, Socket connectedSocket, bool isInitiator, ILogger logger) : base(info, connectedSocket, logger) + { + this.isInitiator = isInitiator; + encBuff = BufferPool.RentBuffer(128000); + decBuff = BufferPool.RentBuffer(128000); + + + keyManager = new EphemeralKeyManager(algo, isInitiator ? KeyRotationPeriodMs : -1); + keyManager.SendData += SendKeyMsg; + + } + + public void SetKeyRotationPeriod(int timeMs) + { + if (isInitiator) + { + KeyRotationPeriodMs = timeMs; + keyManager.SetKeyRotationTime(timeMs); + } + } + + private void SendKeyMsg(MessageFlags flags, byte[] buffer, int offset, int count) + { + FlagAndSend(flags, buffer, offset, count); + } + + protected override int WritePrefix(PooledMemoryStream sendStream) + { + sendStream.WriteByte(keyManager.CurrKeyNumber); + return 1; + } + + protected override int WriteData(byte[] buffer, int offset, int count) + { + EnsureCapacityEnc(count); + + keyManager.GetAlgorithm(keyManager.CurrKeyNumber, out var algo); + int amountEnc = algo.EncryptInto(buffer, offset, count, encBuff, 0); + + return base.WriteData(encBuff, 0, amountEnc); + } + + protected override void HandleReceivedBytes(byte[] buffer, int offset, int count) + { + var flag = (MessageFlags)buffer[offset++]; count--; + + if (flag == MessageFlags.HP || flag == MessageFlags.HPAck) return; + + var keyNo = buffer[offset++]; count--; + + keyManager.GetAlgorithm(keyNo, out var algo); + + EnsureCapacityDec(count); + try + { + count = algo.DecryptInto(buffer, offset, count, decBuff, 0); // not sure if try catch this. + } + catch(Exception e) + { + Log(e); + CloseChannel(); + return; + } + + buffer = decBuff; + offset = 0; + + HandleReceivedMessage(buffer, offset, count, flag); + } + + + + protected override void HandleReceivedMessage(byte[] buffer, int offset, int count, MessageFlags flag) + { + switch (flag) + { + case MessageFlags.KeyExchange: + case MessageFlags.KeyExchangeAck: + case MessageFlags.KeyExchangeFin: + keyManager.HandleMessage(flag, buffer, offset, count); + break; + } + base.HandleReceivedMessage(buffer, offset, count, flag); + } + + private void EnsureCapacityEnc(int count) + { + if(encBuff.Length < count + 256) + { + BufferPool.ReturnBuffer(encBuff); + encBuff = BufferPool.RentBuffer(count + 256); + } + } + private void EnsureCapacityDec(int count) + { + if (decBuff.Length < count + 256) + { + BufferPool.ReturnBuffer(decBuff); + decBuff = BufferPool.RentBuffer(count + 256); + } + } + + protected override void ReleaseResources() + { + keyManager.Close(); + base.ReleaseResources(); + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Channels/SecureUdpChannel.cs b/NetworkLibrary/DistributedP2P/Channels/SecureUdpChannel.cs new file mode 100644 index 0000000..a7de4f9 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/SecureUdpChannel.cs @@ -0,0 +1,126 @@ +using NetworkLibrary.Components; +using NetworkLibrary.Components.Crypto.Algorithms; +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.UDP.Reliable.Components; +using NetworkLibrary.Utils; +using System; +using System.Net; +using System.Net.Sockets; + +namespace NetworkLibrary.DistributedP2P.Channels +{ + public class SecureUdpChannel : UdpChannel + { + EphemeralKeyManager keyManager; + byte[] decryptBuff = new byte[65555]; + private readonly bool isInitiator; + + public int KeyRotationPeriodMs { get; private set; } = 1000;//every minute + + public SecureUdpChannel(Socket udpSocket, IPEndPoint receiveEp, IAesAlgorithm algo, ChannelInfo info, bool isInitiator, ILogger logger) : base(udpSocket, receiveEp, info, logger) + { + keyManager = new EphemeralKeyManager(algo, isInitiator ? KeyRotationPeriodMs : -1); + keyManager.SendData += SendKeyMsg; + + + SenderModule sender = new SenderModule(); + sender.MaxSegmentSize = 1280; + sender.MinWindowSize = 1280 * 2; + this.isInitiator = isInitiator; + } + + public void SetKeyRotationPeriod(int timeMs) + { + if (isInitiator) + { + KeyRotationPeriodMs = timeMs; + keyManager.SetKeyRotationTime(timeMs); + } + } + + private void SendKeyMsg(MessageFlags flag, byte[] buffer, int offset, int count) + { + SendInternalReliable(flag, buffer, offset, count); + } + + protected override void BytesReceived(byte[] buffer, int offset, int count) + { + var flag = (MessageFlags)buffer[offset++]; + + if (flag == MessageFlags.HP || flag == MessageFlags.HPAck) + return; + + var keyNo = buffer[offset++]; + count -= 2; + + keyManager.GetAlgorithm(keyNo, out var algo); + + try + { + count = algo.DecryptInto(buffer, offset, count, decryptBuff, 0); + } + catch + { + Log(LogType.Error, "Decryption failed"); + return; + } + + buffer = decryptBuff; + offset = 0; + + HandleReivedMessage(buffer, offset, count, flag); + } + + protected override void HandleReivedMessage(byte[] buffer, int offset, int count, MessageFlags flag) + { + switch (flag) + { + case MessageFlags.KeyExchange: + case MessageFlags.KeyExchangeAck: + case MessageFlags.KeyExchangeFin: + HandleKeyMessage(buffer, offset, count, flag); + break; + } + + base.HandleReivedMessage(buffer, offset, count, flag); + } + + private void HandleKeyMessage(byte[] buffer, int offset, int count, MessageFlags flag) + { + try + { + keyManager.HandleMessage(flag, buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + + } + + protected override void SendWithFlag(MessageFlags flag, byte[] buffer, int offset, int count) + { + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + stream.WriteByte((byte)flag); + stream.WriteByte(keyManager.CurrKeyNumber); + + stream.Reserve(count + 128); + keyManager.GetAlgorithm(keyManager.CurrKeyNumber, out var algo); + stream.Position32 += algo.EncryptInto(buffer, offset, count, stream.GetBuffer(), 2); + + SendInternal(stream.GetBuffer(), 0, stream.Position32); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + protected override void ReleseResources() + { + base.ReleseResources(); + keyManager.Close(); + } + + } + +} diff --git a/NetworkLibrary/DistributedP2P/Channels/TcpChannel.cs b/NetworkLibrary/DistributedP2P/Channels/TcpChannel.cs new file mode 100644 index 0000000..ef58de3 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/TcpChannel.cs @@ -0,0 +1,379 @@ +using NetworkLibrary.Components; +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using System; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Channels +{ + + public class TcpChannel : IChannel + { + public ChannelInfo Info { get; private set; } + + public event Action OnBytesReceived; + public event Action OnDisconnected; + + private readonly Socket connectedSocket; + private readonly ILogger logger; + private int totalBytesReceived; + private SocketAsyncEventArgs receiveArgs; + private int Closing = 0; + + protected PooledMemoryStream sendStream = new PooledMemoryStream(); + protected PooledMemoryStream flushStream = new PooledMemoryStream(); + + private object bufferMutex = new object(); + private object sendMtex = new object(); + private int sendActive = 0; + private int msgAvailable = 0; + + private SocketAsyncEventArgs sendArgs; + private ByteMessageReader reader = new ByteMessageReader(); + + private KeepAlive keepAlive; + private Pinger pinger; + + public TcpChannel(ChannelInfo info, Socket connectedSocket, ILogger logger) + { + Info = info; + this.connectedSocket = connectedSocket; + this.logger = logger; + InitializeReceiver(); + + sendArgs = new SocketAsyncEventArgs(); + sendArgs.Completed += Sent; + reader.OnMessageReady += HandleReceivedBytes; + + keepAlive = new KeepAlive(); + keepAlive.SendData += FlagAndSend; + keepAlive.NotAlive += () => ErrorAndEnd("Kepp Alive Timed Out"); + + pinger = new Pinger(); + pinger.SendData += FlagAndSend; + } + + + public Task Ping() + { + return pinger.Ping(); + } + + public void Send(byte[] buffer, int offset, int count) + { + FlagAndSend(MessageFlags.StandardMessage, buffer, offset, count); + } + + protected void FlagAndSend(MessageFlags flag, byte[] buffer, int offset, int count) + { + + if (IsSessionClosing()) + return; + + lock (bufferMutex) + { + try + { + int lenghtPos = sendStream.Position32; + sendStream.Position32 += 4; + + sendStream.WriteByte((byte)flag); + int prefixLen = WritePrefix(sendStream); + + int amountWritten = WriteData(buffer, offset, count); + int lastPos = sendStream.Position32; + + sendStream.Position32 = lenghtPos; + sendStream.WriteInt(amountWritten + prefixLen + 1); + sendStream.Position32 = lastPos; + + Interlocked.Exchange(ref msgAvailable, 1); + + } + catch (Exception ex) + { + ErrorAndEnd(ex.Message + "\n" + ex.StackTrace); + return; + } + + + } + SignalSend(); + + + } + + protected virtual int WritePrefix(PooledMemoryStream sendStream) + { + return 0; + } + + protected virtual int WriteData(byte[] buffer, int offset, int count) + { + sendStream.Write(buffer, offset, count); + return count; + } + + private void SignalSend() + { + bool send = false; + lock (sendMtex) + { + if (Interlocked.CompareExchange(ref sendActive, 1, 0) == 0) + { + + lock (bufferMutex) + { + if (Interlocked.CompareExchange(ref msgAvailable, 0, 1) == 1) + { + var temp = sendStream; + sendStream = flushStream; + flushStream = temp; + + sendStream.Position32 = 0; + send = true; + } + else + { + Interlocked.Exchange(ref sendActive, 0); + } + } + } + + } + + if (send) + { + sendArgs.SetBuffer(flushStream.GetBuffer(), 0, flushStream.Position32); + if (!connectedSocket.SendAsync(sendArgs)) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => Sent(null, sendArgs), null); + } + } + } + private void Sent(object sender, SocketAsyncEventArgs e) + { + try + { + if (IsSessionClosing()) + return; + + if (e.SocketError != SocketError.Success) + { + ErrorAndEnd($"While sending a socket error occured {e.SocketError}"); + return; + } + else if (e.BytesTransferred == 0) + { + Log(LogType.Error, "0 bytes Sent"); + CloseChannel(); + return; + } + else if (e.BytesTransferred < e.Count) + { + sendArgs.SetBuffer(e.Offset + e.BytesTransferred, e.Count - e.BytesTransferred); + if (!connectedSocket.SendAsync(sendArgs)) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => Sent(null, sendArgs), null); + } + return; + } + bool send = false; + lock (sendMtex) + { + lock (bufferMutex) + { + if (Interlocked.CompareExchange(ref msgAvailable, 0, 1) == 1) + { + var temp = sendStream; + sendStream = flushStream; + flushStream = temp; + sendStream.Position32 = 0; + + send = true; + } + else + { + Interlocked.Exchange(ref sendActive, 0); + } + } + } + + if (send) + { + sendArgs.SetBuffer(flushStream.GetBuffer(), 0, flushStream.Position32); + + if (!connectedSocket.SendAsync(sendArgs)) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => Sent(null, sendArgs), null); + } + } + } + catch (Exception ex) + { + Log(ex); + CloseChannel(); + } + + + } + + public void Start() + { + Receive(); + } + + private void InitializeReceiver() + { + receiveArgs = new SocketAsyncEventArgs(); + var buff = new byte[1280000]; + receiveArgs.SetBuffer(buff, 0, buff.Length); + receiveArgs.Completed += Received; + + } + + private void Receive() + { + if (!connectedSocket.ReceiveAsync(receiveArgs)) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => Received(null, receiveArgs), null); + } + } + + private void Received(object sender, SocketAsyncEventArgs e) + { + + while (true) + { + if (e.SocketError != SocketError.Success) + { + ErrorAndEnd($"While receiving a socket error occured {e.SocketError}"); + return; + } + else if (e.BytesTransferred == 0) + { + CloseChannel(); + return; + } + + totalBytesReceived += e.BytesTransferred; + try + { + HandleReceived(e.Buffer, e.Offset, e.BytesTransferred); + } + catch (Exception ex) + { + ErrorAndEnd(ex.Message + "\n" + ex.StackTrace); + return; + } + + if (IsSessionClosing()) + return; + + //Receive(); + try + { + if (connectedSocket.ReceiveAsync(receiveArgs)) + { + return; + } + } + catch (Exception ex) + { + if (IsSessionClosing()) + return; + + ErrorAndEnd(ex.Message + "\n" + ex.StackTrace); + } + + } + } + + protected virtual void HandleReceivedBytes(byte[] buffer, int offset, int count) + { + var flag = (MessageFlags)buffer[offset++]; + count--; + HandleReceivedMessage(buffer, offset, count, flag); + } + + protected virtual void HandleReceivedMessage(byte[] buffer, int offset, int count, MessageFlags flag) + { + switch (flag) + { + case MessageFlags.StandardMessage: + PublishBytes(buffer, offset, count); + break; + + case MessageFlags.KeepAliveMessage: + keepAlive.HandleMessage(flag, buffer, offset, count); + break; + + case MessageFlags.Pong: + case MessageFlags.Ping: + pinger.HandleMessage(flag, buffer, offset, count); + break; + } + } + + protected void PublishBytes(byte[] buffer, int offset, int count) + { + OnBytesReceived?.Invoke(buffer, offset, count); + } + + public void CloseChannel() + { + Console.WriteLine("Closing channel"); + if (Interlocked.CompareExchange(ref Closing, 1, 0) == 0) + { + ReleaseResources(); + } + } + + protected virtual void ReleaseResources() + { + try + { + connectedSocket.Shutdown(SocketShutdown.Both); + } + catch { } + keepAlive.Close(); + OnDisconnected?.Invoke(); + } + + + private void HandleReceived(byte[] buffer, int offset, int bytesTransferred) + { + reader.ParseBytes(buffer, offset, bytesTransferred); + } + + + + protected void ErrorAndEnd(string errMsg) + { + Log(LogType.Debug,errMsg); + CloseChannel(); + } + + + protected virtual void Log(LogType logType, string v) + { + logger?.Log(logType, v); + } + + protected virtual void Log(Exception e) + { + logger?.Log(e); + } + + + + private bool IsSessionClosing() + { + return Interlocked.CompareExchange(ref Closing, 0, 0) == 1; + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Channels/UdpChannel.cs b/NetworkLibrary/DistributedP2P/Channels/UdpChannel.cs new file mode 100644 index 0000000..e8fcf75 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Channels/UdpChannel.cs @@ -0,0 +1,375 @@ +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.UDP.Jumbo; +using NetworkLibrary.UDP.Reliable.Components; +using NetworkLibrary.Utils; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Channels +{ + public class UdpChannel : IChannel + { + public ChannelInfo Info { get; private set; } + + private UdpChannelBase innerchannel; + + public event Action OnBytesReceived; + public event Action OnDisconnected; + + protected JumboModule JumboUdp = new JumboModule(0); + internal ReliableModule ReliableUdp; + private ReliableModule internalReliableModule; + protected KeepAlive keepAlive; + protected Pinger pinger; + + private int isClosed = 0; + private int isDisposed = 0; + private readonly ILogger logger; + + public UdpChannel(Socket udpSocket, IPEndPoint receiveEp, ChannelInfo info, ILogger logger) + { + + Info = info; + this.logger = logger; + innerchannel = new UdpChannelBase(udpSocket, receiveEp, info, logger); + + JumboUdp.SendToSocket = SendJumboSegment; + JumboUdp.MessageReceived = HandleMessage; + + SenderModule sender = new SenderModule(); + sender.MaxSegmentSize = 1280; + sender.MinWindowSize = 1280 * 2; + + ReliableUdp = new ReliableModule(receiveEp, sender); + ReliableUdp.OnReceived += (e, b, o, c) => HandleMessage(b, o, c); + ReliableUdp.OnSend += SendRudpSegment; + + SenderModule sender2 = new SenderModule(); + sender.MaxSegmentSize = 1280; + sender.MinWindowSize = 1280 * 2; + + internalReliableModule = new ReliableModule(receiveEp, sender2); + internalReliableModule.OnReceived += (e, b, o, c) => HandleInternalReliableMessage(b, o, c); + internalReliableModule.OnSend += SendInternalRudpSegment; + + keepAlive = new KeepAlive(); + keepAlive.SendData += SendInternalReliable; + keepAlive.NotAlive += HandleDisconnect; + + pinger = new Pinger(); + pinger.SendData += SendInternalReliable; + + + } + + public void Start() + { + innerchannel.OnBytesReceived += BytesReceived; + innerchannel.OnDisconnected += HandleDisconnect; + innerchannel.Start(); + } + + protected virtual void BytesReceived(byte[] buffer, int offset, int count) + { + // filter flags + var flag = (MessageFlags)buffer[offset++]; + count--; + HandleReivedMessage(buffer, offset, count, flag); + } + + protected virtual void HandleReivedMessage(byte[] buffer, int offset, int count, MessageFlags flag) + { + + switch (flag) + { + case MessageFlags.StandardMessage: + HandleMessage(buffer, offset, count); + break; + + case MessageFlags.JumboMessage: + HandleJumboSegment(buffer, offset, count); + break; + + case MessageFlags.ReliableMessage: + HandleRudpSegment(buffer, offset, count); + break; + + case MessageFlags.KeepAliveMessage: + HandleKeepAliveMessage(buffer, offset, count, flag); + break; + + case MessageFlags.Kill: + HandleDisconnect(); + break; + + case MessageFlags.InternalReliableMessage: + HandleIncomingInternalRudpSegment(buffer, offset, count); + break; + + case MessageFlags.Ping: + case MessageFlags.Pong: + HandlePingMessage(buffer, offset, count, flag); + break; + + } + + + } + + + private void HandleInternalReliableMessage(byte[] buffer, int offset, int count) + { + var flag = (MessageFlags)buffer[offset++]; + count--; + + HandleReivedMessage(buffer, offset, count, flag); + } + + private void HandleMessage(byte[] buffer, int offset, int count) + { + try + { + OnBytesReceived?.Invoke(buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + throw; + } + } + + private void HandleJumboSegment(byte[] buffer, int offset, int count) + { + try + { + JumboUdp.HandleReceivedSegment(buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + } + + private void SendJumboSegment(byte[] arg1, int arg2, int arg3) + { + try + { + SendWithFlag(MessageFlags.JumboMessage, arg1, arg2, arg3); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + + } + + private void HandleRudpSegment(byte[] buffer, int offset, int count) + { + try + { + ReliableUdp.HandleBytes(buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + + } + private void HandleKeepAliveMessage(byte[] buffer, int offset, int count, MessageFlags flag) + { + try + { + keepAlive.HandleMessage(flag, buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + } + + private void HandleIncomingInternalRudpSegment(byte[] buffer, int offset, int count) + { + try + { + internalReliableModule.HandleBytes(buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + } + + private void HandlePingMessage(byte[] buffer, int offset, int count, MessageFlags flag) + { + try + { + pinger.HandleMessage(flag, buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + + } + + private void SendRudpSegment(ReliableModule module, byte[] buffer, int offset, int count) + { + try + { + SendWithFlag(MessageFlags.ReliableMessage, buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + } + + + private void SendInternalRudpSegment(ReliableModule module, byte[] buffer, int offset, int count) + { + try + { + SendWithFlag(MessageFlags.InternalReliableMessage, buffer, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + } + + } + + public Task Ping() + { + return pinger.Ping(); + } + + public virtual void Send(byte[] buffer, int offset, int count) + { + if (count > 64000) + { + JumboUdp.Send(buffer, offset, count); + } + else + { + SendWithFlag(MessageFlags.StandardMessage, buffer, offset, count); + } + + } + + public void SendReliable(byte[] buffer, int offset, int count) + { + ReliableUdp.Send(buffer, offset, count); + } + + + protected virtual void SendWithFlag(MessageFlags flag, byte[] arg1, int arg2, int arg3) + { + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + stream.WriteByte((byte)flag); + stream.Write(arg1, arg2, arg3); + SendInternal(stream.GetBuffer(), 0, stream.Position32); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + protected void SendInternalReliable(MessageFlags flag, byte[] buffer, int offset, int count) + { + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + stream.WriteByte((byte)flag); + stream.Write(buffer, offset, count); + + internalReliableModule.Send(stream.GetBuffer(), 0, stream.Position32); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + + protected void SendInternal(byte[] bytes, int offset, int count) + { + try + { + innerchannel.Send(bytes, offset, count); + } + catch (Exception e) + { + Log(e); + CloseChannel(); + throw; + } + } + + + public void CloseChannel() + { + try + { + SendWithFlag(MessageFlags.Kill, new byte[1], 0, 1); + + } + catch { } + + try + { + innerchannel.CloseChannel(); + } + catch { } + + HandleDisconnect(); + + } + + protected void HandleDisconnect() + { + if (Interlocked.CompareExchange(ref isClosed, 1, 0) == 0) + { + Log(LogType.Debug,"Udp Channel disconnected"); + OnDisconnected?.Invoke(); + Dispose(); + } + + } + + public void Dispose() + { + if (Interlocked.CompareExchange(ref isDisposed, 1, 0) == 0) + ReleseResources(); + } + + protected virtual void ReleseResources() + { + ReliableUdp.Close(); + internalReliableModule.Close(); + keepAlive.Close(); + JumboUdp.Release(); + + innerchannel.CloseChannel(); + + OnDisconnected = null; + OnBytesReceived = null; + + + } + + protected virtual void Log(LogType logType, string v) + { + logger?.Log(logType, v); + } + + protected virtual void Log(Exception e) + { + logger?.Log(e); + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Client/ChannelInfo.cs b/NetworkLibrary/DistributedP2P/Client/ChannelInfo.cs new file mode 100644 index 0000000..e8795d9 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/ChannelInfo.cs @@ -0,0 +1,41 @@ +using NetworkLibrary.Components.Crypto; +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.Client +{ + public enum ChannelType + { + Tcp,SecureTcp,Udp,SecureUdp + } + + public enum TcpHolePunchStrategy + { + Sequential, Simultaneous + } + public class ChannelInfo + { + public ChannelInfo() + { + } + + public ChannelInfo(ChannelType channelType, string channelName) + { + ChannelType = channelType; + ChannelName = channelName; + } + + public ChannelType ChannelType { get; internal set; } + + public string ChannelName { get; internal set; } = ""; + + internal bool RequiresKeyExchange() + { + if(ChannelType == ChannelType.SecureTcp || + ChannelType == ChannelType.SecureUdp) + return true; + return false; + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Client/DistributedLobbyClient.cs b/NetworkLibrary/DistributedP2P/Client/DistributedLobbyClient.cs new file mode 100644 index 0000000..7644a14 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/DistributedLobbyClient.cs @@ -0,0 +1,438 @@ +using NetworkLibrary.Components.Crypto; +using NetworkLibrary.Components.Crypto.DiffieHellman; +using NetworkLibrary.Components.Crypto.KeyDerivation; +using NetworkLibrary.DistributedP2P.Client.StateManagement; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.DistributedP2P.Server; +using NetworkLibrary.MessageProtocol; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.TCP.AES; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using NetworkLibrary.Components; +using System.Net; +using NetworkLibrary.DistributedP2P.Channels.Components; + +namespace NetworkLibrary.DistributedP2P.Client +{ + public class DistributedLobbyClient : IDistributedConnection, IDisposable where S : ISerializer, new() + { + IClientDbConnection clientDbConnector; + IClientAuthenticationProvider clientAuthProvider; + SecureMessageClient sslClient; + StateManager stateManager; + TimeSync timeSync; + + private ConcurrentDictionary onlinePeers = new ConcurrentDictionary(); + + public event Action PeerConnected; + public event Action PeerOnline; + public event Action PeerOffline; + + public event Action MessageReceived; + public event Action Disconnected; + + public Guid SessionId { get; private set; } + + private EndpointData DiscoveryServerEndpoint; + private int connected = 0; + public bool IsConnected + { + get => Interlocked.CompareExchange(ref connected, 0, 0) == 1; + private set => Interlocked.Exchange(ref connected, value ? 1 : 0); + } + + private EndpointData serverEndpoint= new EndpointData(); + + bool isDisposed = false; + ILogger logger; + DateTime lastKeepAlive = DateTime.Now; + + public DistributedLobbyClient(IClientDbConnection clientDbConnector, + IClientAuthenticationProvider clientAuthProvider, + ILogger logger, + X509Certificate2 certificate = null) + { + this.clientDbConnector = clientDbConnector; + this.clientAuthProvider = clientAuthProvider; + this.logger=logger; + + stateManager = new StateManager(logger); + + sslClient = new SecureMessageClient(certificate); + sslClient.OnMessageReceived += HandleServerMsg; + sslClient.OnDisconnected += HandleDisconnected; + timeSync = new TimeSync(this); + } + + public async Task ConnectAsync(string ip, int port) + { + if (isDisposed) + throw new ObjectDisposedException(this.ToString()); + + if (IsConnected) + return true; + + IClientAuthenticationToken authToken = clientAuthProvider.Authenticate(); + + bool res = await sslClient.ConnectAsync(ip, port).ConfigureAwait(false); + if (res) + { + serverEndpoint = new EndpointData(ip, port); + timeSync.SetEndpoint(serverEndpoint); + + Guid conversationId = Guid.NewGuid(); + var conState = new ClientConnectionState(conversationId, this, clientDbConnector, authToken, logger); + stateManager.RegisterState(conState); + + conState.OnComplete += (_) => + { + if (conState.IsSuccesful) + { + SessionId = conState.SessionId; + DiscoveryServerEndpoint = new EndpointData(ip, conState.EDSPort); + Console.WriteLine($"Connected to server {ip}:{port} with session {SessionId} and discovery port {conState.EDSPort}"); + IsConnected = true; + timeSync.StartAutoTimeSync(); + StartKeepAlive(); + } + }; + + conState.Start(); + + await conState.WaitCompletion().ConfigureAwait(false); + return conState.IsSuccesful; + + } + else return false; + } + + private async void StartKeepAlive() + { + try + { + lastKeepAlive = DateTime.Now; + while (IsConnected) + { + await Task.Delay(4000).ConfigureAwait(false); + + MessageEnvelope envelope = new MessageEnvelope(); + envelope.Header = InternalConstants.KeepAlive; + envelope.IsInternal = true; + SendAsyncMessage(envelope); + + if((DateTime.Now- lastKeepAlive).TotalMilliseconds > 10000) + { + Disconnect(); + return; + } + } + } + catch + { + Disconnect(); + } + + } + + #region Send + public void SendAsyncMessage(MessageEnvelope message) + { + sslClient.SendAsyncMessage(message); + } + + public Task SendMessageAndWaitResponse(MessageEnvelope message) + { + return sslClient.SendMessageAndWaitResponse(message); + } + public void SendAsyncMessage(Guid a, MessageEnvelope msg) + { + msg.To = a; + sslClient.SendAsyncMessage(msg); + } + + public Task SendMessageAndWaitResponse(Guid a, MessageEnvelope msg) + { + msg.To = a; + return sslClient.SendMessageAndWaitResponse(msg); + } + #endregion + + private void HandleServerMsg(MessageEnvelope envelope) + { + if (envelope.IsInternal) + { + if (stateManager.HandleMessage(envelope)) + return; + + switch (envelope.Header) + { + case InternalConstants.PipeRequestTcp: + case InternalConstants.PipeRequestUdp: + + var pipeState = new ClientPipeState(envelope, this, serverEndpoint, logger); + pipeState.OnComplete += HandlePipeCreated; + stateManager.RegisterState(pipeState); + pipeState.HandleMessage(envelope); + break; + + + case InternalConstants.PublishPeerList: + + var buffer = envelope.Payload; + int offset = envelope.PayloadOffset; + + PeerStatusList statusList = KnownTypeSerializer.DeserializePeerStatusList(buffer, ref offset); + PublishPeerStatusEvents(statusList); + + break; + + case InternalConstants.RequestHolepunchUdp: + ManageUdpHolepunchRequest(envelope); + break; + + case InternalConstants.RequestSequentialHolepunchTcp: + ManageSequentialTcpHolepunchReq(envelope); + break; + + case InternalConstants.RequestSimultaneousHolepunchTcp: + ManageSimyltaneousTcpHolepunchReq(envelope); + break; + + case InternalConstants.KeepAlive: + lastKeepAlive= DateTime.Now; + break; + + } + } + else + { + MessageReceived?.Invoke(envelope); + } + + + } + + + + private void PublishPeerStatusEvents(PeerStatusList statusList) + { + foreach (var offlineKV in statusList.WentOffline) + { + if (onlinePeers.TryRemove(offlineKV.Value.EphemeralId, out _)) + PeerOffline?.Invoke(offlineKV.Value); + } + + foreach (var onlineKV in statusList.NewOnline) + { + if (onlinePeers.TryAdd(onlineKV.Value.EphemeralId, onlineKV.Value)) + PeerOnline?.Invoke(onlineKV.Value); + } + } + + public Dictionary GetPeerList() + { + Dictionary copy = new Dictionary(); + foreach (var peerKv in onlinePeers) + { + var sts = new PeerStatus(); + sts.EphemeralId = peerKv.Value.EphemeralId; + sts.PeerId = peerKv.Value.PeerId; + sts.OnlineSince = peerKv.Value.OnlineSince; + + copy[peerKv.Key] = sts; + } + return copy; + } + + public async Task OpenRelayChannel(Guid destinationPeer, ChannelInfo Info) + { + var pipeState = new ClientPipeState(Guid.NewGuid(), this, serverEndpoint, Info, logger); + stateManager.RegisterState(pipeState); + pipeState.Start(destinationPeer); + + await pipeState.WaitCompletion().ConfigureAwait(false); + + if (pipeState.IsSuccesful) + { + IChannel channel = ChannelFactory.CreateChannel(pipeState, true, logger); + return channel; + } + return null; + } + + private void HandlePipeCreated(IConversationState state) + { + if (state.IsSuccesful) + { + var pipeState = (ClientPipeState)state; + IChannel channel = ChannelFactory.CreateChannel(pipeState, false, logger); + + if (channel != null) + PeerConnected?.Invoke(channel); + + Console.WriteLine("DestPeer Conn Succesfull"); + // notify that a connection is opened, like socket accept + } + } + + public async Task TryHolePunch(Guid destination, ChannelInfo info, TcpHolePunchStrategy strategy = TcpHolePunchStrategy.Sequential) + { + + if(info.ChannelType == ChannelType.Udp || info.ChannelType == ChannelType.SecureUdp) + { + var state = new ClientUdpHolepunchState(Guid.NewGuid(), destination, this, serverEndpoint, DiscoveryServerEndpoint, info, logger); + stateManager.RegisterState(state); + state.Start(); + + await state.WaitCompletion().ConfigureAwait(false); + + if (state.IsSuccesful) + { + return ChannelFactory.CreateChannel(state, true, logger); + } + return null; + } + else + { + if (strategy == TcpHolePunchStrategy.Sequential) + { + var state = new ClientSequentialTcpHolepunchState(Guid.NewGuid(), destination, this, serverEndpoint, DiscoveryServerEndpoint, info, logger); + stateManager.RegisterState(state); + state.Start(); + + await state.WaitCompletion().ConfigureAwait(false); + + if (state.IsSuccesful) + { + return ChannelFactory.CreateChannel(state, true, logger); + } + return null; + } + else + { + var state = new ClientSimultaneousTcpHolepunchState(Guid.NewGuid(), destination, this, serverEndpoint, DiscoveryServerEndpoint, info, logger); + stateManager.RegisterState(state); + state.Start(); + + await state.WaitCompletion().ConfigureAwait(false); + + if (state.IsSuccesful) + { + return ChannelFactory.CreateChannel(state, true, logger); + } + return null; + } + + } + + } + + private void ManageUdpHolepunchRequest(MessageEnvelope envelope) + { + var state = new ClientUdpHolepunchState(envelope.MessageId, envelope.From, this,serverEndpoint,DiscoveryServerEndpoint, null, logger); + stateManager.RegisterState(state); + state.OnComplete += State_OnComplete; + state.HandleMessage(envelope); + + void State_OnComplete(IConversationState obj) + { + if (obj.IsSuccesful) + { + var ch = ChannelFactory.CreateChannel(state, false, logger); + PeerConnected?.Invoke(ch); + } + + } + + } + + private void ManageSimyltaneousTcpHolepunchReq(MessageEnvelope envelope) + { + var state = new ClientSimultaneousTcpHolepunchState(envelope.MessageId, envelope.From, this, serverEndpoint, DiscoveryServerEndpoint, null, logger); + stateManager.RegisterState(state); + state.OnComplete += State_OnComplete; + state.HandleMessage(envelope); + + void State_OnComplete(IConversationState obj) + { + if (obj.IsSuccesful) + { + var ch = ChannelFactory.CreateChannel(state, false, logger); + PeerConnected?.Invoke(ch); + } + } + } + + private void ManageSequentialTcpHolepunchReq(MessageEnvelope envelope) + { + var state = new ClientSequentialTcpHolepunchState(envelope.MessageId, envelope.From, this, serverEndpoint, DiscoveryServerEndpoint, null, logger); + stateManager.RegisterState(state); + state.OnComplete += State_OnComplete; + state.HandleMessage(envelope); + + void State_OnComplete(IConversationState obj) + { + if (obj.IsSuccesful) + { + var ch = ChannelFactory.CreateChannel(state, false, logger); + PeerConnected?.Invoke(ch); + } + } + } + + //public Task JoinRoom() + //{ + // return Task.FromResult(new RoomConnection()); + //} + + public double GetTime() + { + return timeSync.GetTime(); + } + + public DateTime GetDateTime() + { + return timeSync.GetDateTime(); + } + + public Task SyncTime() + { + return timeSync.SyncTime(); + } + + public void Disconnect() + { + IsConnected= false; + sslClient.Disconnect(); + } + + private void HandleDisconnected() + { + IsConnected = false; + + timeSync.StopAutoTimeSync(); + Disconnected?.Invoke(); + } + + public void Dispose() + { + if (isDisposed) + return; + + isDisposed = true; + try + { + sslClient?.Dispose(); + timeSync?.Dispose(); + } + catch { } + + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Client/IClientAuthenticationProvider.cs b/NetworkLibrary/DistributedP2P/Client/IClientAuthenticationProvider.cs new file mode 100644 index 0000000..b876718 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/IClientAuthenticationProvider.cs @@ -0,0 +1,7 @@ +namespace NetworkLibrary.DistributedP2P.Client +{ + public interface IClientAuthenticationProvider + { + IClientAuthenticationToken Authenticate(); + } +} \ No newline at end of file diff --git a/NetworkLibrary/DistributedP2P/Client/IClientAuthenticationToken.cs b/NetworkLibrary/DistributedP2P/Client/IClientAuthenticationToken.cs new file mode 100644 index 0000000..f6627d9 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/IClientAuthenticationToken.cs @@ -0,0 +1,9 @@ +namespace NetworkLibrary.DistributedP2P.Client +{ + public interface IClientAuthenticationToken + { + string Token { get; } + string AuthenticationMethod { get; } + string AdditionalData { get; } + } +} \ No newline at end of file diff --git a/NetworkLibrary/DistributedP2P/Client/IClientDbConnection.cs b/NetworkLibrary/DistributedP2P/Client/IClientDbConnection.cs new file mode 100644 index 0000000..239c2a6 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/IClientDbConnection.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.Client +{ + public interface IClientDbConnection + { + byte[] GetClientPublicData(); + } +} diff --git a/NetworkLibrary/DistributedP2P/Client/RoomConnection.cs b/NetworkLibrary/DistributedP2P/Client/RoomConnection.cs new file mode 100644 index 0000000..5a6a56f --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/RoomConnection.cs @@ -0,0 +1,79 @@ +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.Utils; +using System; +using System.Collections.Concurrent; + +namespace NetworkLibrary.DistributedP2P.Client +{ + public class RoomConnection + { + internal IChannel communicationChannel; + internal byte assignedId; + internal Guid roomId; + + ConcurrentDictionary aliasMap = new ConcurrentDictionary(); + + public event Action ConnectionLost; + public event Action MessageReceived; + public event Action PeerJoined; + public event Action PeerLeft; + + internal event Action LeaveRequested; + + public RoomConnection(IChannel communicationChannel, Guid roomId, byte assignedId) + { + this.communicationChannel = communicationChannel; + this.roomId = roomId; + this.assignedId = assignedId; + + communicationChannel.OnBytesReceived += BytesReceived; + } + // server should call this directly + internal void PeerJoinedRoom(Guid peerId, byte alias) + { + aliasMap.TryAdd(alias, peerId); + PeerJoined?.Invoke(peerId); + } + + internal void PeerLeftRoom(Guid peerId, byte alias) + { + aliasMap.TryRemove(alias, out _); + PeerLeft?.Invoke(peerId); + } + + private void BytesReceived(byte[] buffer, int offset, int count) + { + byte alias = buffer[offset++]; + count--; + + if (aliasMap.TryGetValue(alias, out var guid)) + { + MessageReceived?.Invoke(guid, buffer, offset, count); + } + } + + public void Send(byte[] buffer, int offset, int count) + { + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + stream.WriteByte(assignedId); + stream.Write(buffer, offset, count); + + communicationChannel.Send(stream.GetBuffer(), 0, stream.Position32); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + //public void SendReliable(byte[] buffer, int offset, int count) + //{ + // if(communicationChannel.Info.ChannelType == ChannelType.Udp) + // ((UdpChannel)communicationChannel).SendReliable() + //} + + + public void LeaveRoom() + { + + } + + } +} \ No newline at end of file diff --git a/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientConnectionState.cs b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientConnectionState.cs new file mode 100644 index 0000000..6ed3641 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientConnectionState.cs @@ -0,0 +1,105 @@ +using NetworkLibrary.DistributedP2P.Components; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Client.StateManagement +{ + //Signed Challenge Tokens + internal class ClientConnectionState : ConversationStateBase + { + private readonly IDistributedConnection connection; + private readonly IClientDbConnection clientDbConnector; + private readonly IClientAuthenticationToken authToken; + private TaskCompletionSource timeSyncComplete = new TaskCompletionSource(); + + public Guid SessionId { get; private set; } + public int EDSPort { get; private set; } + public ClientConnectionState(Guid stateId, IDistributedConnection connection, IClientDbConnection clientDbConnector, IClientAuthenticationToken authToken, ILogger logger) : base(stateId, 20000, logger) + { + this.connection = connection; + this.clientDbConnector = clientDbConnector; + this.authToken = authToken; + } + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.ConnectionGetClientPublicData: + SendClientPublicData(message); + break; + case InternalConstants.ConnectionAckGood: + HandleConnectionSucces(message); + break; + case InternalConstants.Error: + HandleConnectionFail(message); + break; + } + } + + public void Start() + { + MessageEnvelope msg = CreateEnvelope(); + msg.Header = InternalConstants.ConnectionReq; + msg.KeyValuePairs = new Dictionary(); + + msg.KeyValuePairs.Add("AuthToken", authToken.Token); + msg.KeyValuePairs.Add("AuthMethod", authToken.AuthenticationMethod); + msg.KeyValuePairs.Add("AdditionalData", authToken.AdditionalData); + + List locals = IPHelper.GetLocalIPAddresses4(); + int i = 0; + foreach (var ip in locals) + { + msg.KeyValuePairs[ip] = null; + } + + connection.SendAsyncMessage(msg); + SyncTime(); + // now server will authenticate after this + // may ask additional data to link, if we are first timer + // then succes or fail + } + + private void SyncTime() + { + var task = connection.SyncTime().ContinueWith(t => + { + MessageEnvelope msg = CreateEnvelope(); + msg.Header = InternalConstants.SyncTime; + connection.SendAsyncMessage(msg); + + }, TaskScheduler.Default); + task.ConfigureAwait(false); + + } + + private void SendClientPublicData(MessageEnvelope message) + { + MessageEnvelope response = CreateEnvelope(); + response.Header = InternalConstants.ConnectionAckClientPublicData; + response.Payload = clientDbConnector.GetClientPublicData(); + + connection.SendAsyncMessage(response); + } + + + private void HandleConnectionSucces(MessageEnvelope message) + { + SessionId = message.To; + EDSPort = int.Parse(message.KeyValuePairs["EDPort"]); + Completed(succes: true); + } + + private void HandleConnectionFail(MessageEnvelope message) + { + if (message.KeyValuePairs.ContainsKey("Error")) + ErrorMessage = message.KeyValuePairs["Error"]; + + Completed(succes: false); + } + + + } +} diff --git a/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientPipeState.cs b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientPipeState.cs new file mode 100644 index 0000000..05a2ba2 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientPipeState.cs @@ -0,0 +1,411 @@ +using NetworkLibrary.Components.Crypto.DiffieHellman; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Client.StateManagement +{ + class ClientPipeData + { + public ChannelInfo ChannelInfo { get; set; } + public byte[] DHPublic; + } + internal class ClientPipeState : ConversationStateBase + { + private readonly IDistributedConnection connection; + private readonly EndpointData serverEndpoint; + private Guid destinationPeer; + + public Socket ConnectedSocket { get; private set; } + public EndpointData SuccesfullEndpoint { get; private set; } + public byte[] sharedSecret { get; private set; } + + public ChannelInfo ChannelInfo; + private DiffieHellman df; + private bool isInitiator; + + public ClientPipeState(Guid stateId, IDistributedConnection connection, EndpointData serverEndpoint, ChannelInfo info, ILogger logger) : base(stateId, 20000, logger) + { + this.connection = connection; + this.serverEndpoint = serverEndpoint; + this.ChannelInfo = info; + isInitiator = true; + } + + public ClientPipeState(MessageEnvelope message, IDistributedConnection connection, EndpointData serverEndpoint, ILogger logger) : base(message.MessageId, 20000, logger) + { + this.connection = connection; + this.serverEndpoint = serverEndpoint; + } + + public void Start(Guid destinationPeer) + { + bool tcp = ChannelInfo.ChannelType == ChannelType.Tcp || ChannelInfo.ChannelType == ChannelType.SecureTcp; + this.destinationPeer = destinationPeer; + + var msg = CreateEnvelope(); + msg.Header = tcp ? InternalConstants.PipeRequestTcp : InternalConstants.PipeRequestUdp; + msg.To = destinationPeer; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + var data = GetPipeData(); + + KnownTypeSerializer.SerializeClientPipeData(stream, data); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + + connection.SendAsyncMessage(msg); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + + Log(LogType.Debug, "Requested Pipe"); + } + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + // the listener + case InternalConstants.PipeRequestTcp: + case InternalConstants.PipeRequestUdp: + HandleConnectionRequest(message); + break; + + case InternalConstants.PipeTokenDeliveryTcp: + HandlePipeTokenTcp(message); + break; + + case InternalConstants.PipeTokenDeliveryUdp: + HandlePipeTokenUdp(message); + break; + + case InternalConstants.ConnectionAckGood: + HandleGoodAck(message); + break; + + case InternalConstants.ConnectionAckBad: + HandleBadAck(message); + break; + } + } + + private void HandleConnectionRequest(MessageEnvelope message) + { + Log(LogType.Debug, "Connection Request Received"); + int offs = message.PayloadOffset; + var pipeData = KnownTypeSerializer.DeserializeClientPipeData(message.Payload, ref offs); + ChannelInfo = pipeData.ChannelInfo; + + var myData = GetPipeData(); + + if (ChannelInfo.RequiresKeyExchange()) + { + GenerateSharedSecret(pipeData.DHPublic); + } + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PipeReqAck; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + myData.ChannelInfo = null; + + KnownTypeSerializer.SerializeClientPipeData(stream, myData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + + connection.SendAsyncMessage(msg); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + + } + + private async void HandlePipeTokenTcp(MessageEnvelope message) + { + Log(LogType.Debug, "Handling Tcp Token"); + try + { + int off = message.PayloadOffset; + var pipeData = KnownTypeSerializer.DeserializePipeData(message.Payload, ref off); + + if (pipeData.DHPublic != null) + GenerateSharedSecret(pipeData.DHPublic); + + EndpointData endpoint = pipeData.PipeEndpoint; + + if (IPHelper.IsZero(endpoint.Ip)) + endpoint.Ip = serverEndpoint.Ip; + + Socket connected = await TryConnectWithTimeout(endpoint).ConfigureAwait(false); + if (connected != null) + { + bool success = await TokenExchange(connected, pipeData.Token); + if (success) + { + OnConnectionSuccessful(endpoint, connected); + return; + } + else + { + try { connected.Close(); connected.Dispose(); } catch { } + } + } + // connect send token + //wait a data to come + //then send ack + + Log(LogType.Warning, $"Failed to exchange Tcp Token"); + OnConnectionFail(); + return; + } + catch (Exception e) + { + Log(LogType.Exception, $"An Error occured while handling Tcp Token{e.Message}\n{e.StackTrace}"); + OnConnectionFail(); + } + + } + + private void GenerateSharedSecret(byte[] otherPublic) + { + sharedSecret = df.CalculateSharedSecret(otherPublic); + } + + private async Task TryConnectWithTimeout(EndpointData endpoint, int timeout = 5000) + { + var clientSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); + + var connectTask = ConnectAsync(clientSocket, endpoint.ToIpEndpoint()); + var timeoutTask = Task.Delay(timeout); + + var completedTask = await Task.WhenAny(connectTask, timeoutTask); + + if (completedTask == timeoutTask) + { + Log(LogType.Warning, "Connection Failed"); + try { clientSocket.Close(); clientSocket.Dispose(); } catch { } + return null; + } + + bool res = connectTask.Result; + if (res) + { + return clientSocket; + } + else + { + Log(LogType.Warning, "Connection Failed"); + try { clientSocket.Close(); clientSocket.Dispose(); } catch { } + return null; + } + + } + + private Task ConnectAsync(Socket socket, IPEndPoint endPoint) + { + var tcs = new TaskCompletionSource(); + + var sa = new SocketAsyncEventArgs(); + sa.RemoteEndPoint = endPoint; + sa.Completed += (s, e) => + { + if (e.SocketError == SocketError.Success) + { + tcs.TrySetResult(true); + } + else + { + tcs.TrySetResult(false); + } + sa.Dispose(); + }; + + if (!socket.ConnectAsync(sa)) + { + if (sa.SocketError == SocketError.Success) + tcs.TrySetResult(true); + } + + return tcs.Task; + } + + private async Task TokenExchange(Socket connectedSocket, byte[] token, int timeoutMs = 5000) + { + try + { + int bytesSent = await connectedSocket.SendAsync(new ArraySegment(token), SocketFlags.None); + if (bytesSent != token.Length) + { + Log(LogType.Warning, "Tcp Token Send Failure"); + return false; + } + + var responseBuffer = new byte[1]; + var receiveTask = connectedSocket.ReceiveAsync(new ArraySegment(responseBuffer), SocketFlags.None); + var timeoutTask = Task.Delay(timeoutMs); + + var completedTask = await Task.WhenAny(receiveTask, timeoutTask); + + if (completedTask == timeoutTask) + { + Log(LogType.Warning, "Tcp Token Response Timeout"); + return false; + } + + int bytesReceived = receiveTask.Result; + return bytesReceived == 1; + + } + catch (Exception e) + { + Log(LogType.Exception, $"An Error occured while exchanging Tcp Token{e.Message}\n{e.StackTrace}"); + return false; + } + } + + + + private async void HandlePipeTokenUdp(MessageEnvelope message) + { + try + { + int off = message.PayloadOffset; + var pipeData = KnownTypeSerializer.DeserializePipeData(message.Payload, ref off); + + if (pipeData.DHPublic != null) + GenerateSharedSecret(pipeData.DHPublic); + + + var connected = new Socket(SocketType.Dgram, ProtocolType.Udp); + connected.SendBufferSize = 12800000; + connected.ReceiveBufferSize = 12800000; + + EndpointData endpoint = pipeData.PipeEndpoint; + + + if (IPHelper.IsZero(endpoint.Ip)) + endpoint.Ip = serverEndpoint.Ip; + + bool success = await UdpTokenExchange(connected, pipeData.Token, endpoint.ToIpEndpoint()); + if (success) + { + Log(LogType.Debug, $"Connected"); + OnConnectionSuccessful(endpoint, connected); + return; + } + else + { + try { connected.Close(); connected.Dispose(); } catch { } + } + + // connect send token + //wait a data to come + //then send ack + + + Log(LogType.Warning, $"Failed to exchange Udp Token"); + OnConnectionFail(); + return; + } + catch (Exception e) + { + Log(LogType.Exception, $"An Error occured while handling Udp Token{e.Message}\n{e.StackTrace}"); + OnConnectionFail(); + } + + } + + private async Task UdpTokenExchange(Socket udpSocket, byte[] token, IPEndPoint remoteEndPoint, int timeoutMs = 5000) + { + try + { + udpSocket.Connect(remoteEndPoint); + + var sendTask = udpSocket.SendAsync(new ArraySegment(token), SocketFlags.None); + var sendTimeout = Task.Delay(timeoutMs); + + if (await Task.WhenAny(sendTask, sendTimeout).ConfigureAwait(false) == sendTimeout || + sendTask.Result != token.Length) + { + Log(LogType.Warning, "Udp Token Send Timeout"); + return false; + } + + var responseBuffer = new byte[1024]; + var receiveTask = udpSocket.ReceiveAsync(new ArraySegment(responseBuffer), SocketFlags.None); + var receiveTimeout = Task.Delay(timeoutMs); + + if (await Task.WhenAny(receiveTask, receiveTimeout).ConfigureAwait(false) == receiveTimeout) + { + Log(LogType.Exception,"Udp Token Receive Timeout"); + return false; + } + + return receiveTask.Result == 1; + } + catch (Exception e) + { + Log(LogType.Exception,$"An Error occured while exchanging Udp Token{e.Message}\n{e.StackTrace}"); + return false; + } + + } + + + private void OnConnectionSuccessful(EndpointData endpoint, Socket socket) + { + if (IsCompleted()) + return; + + this.ConnectedSocket = socket; + this.SuccesfullEndpoint = endpoint; + + Log(LogType.Debug, "Completed"); + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.ConnectionAckGood; + connection.SendAsyncMessage(msg); + } + + private void OnConnectionFail() + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.ConnectionAckBad; + connection.SendAsyncMessage(msg); + + Log(LogType.Debug,"Failed"); + + Completed(false); + } + + private void HandleGoodAck(MessageEnvelope message) + { + Completed(true); + } + + private void HandleBadAck(MessageEnvelope message) + { + Completed(false); + } + + private ClientPipeData GetPipeData() + { + ClientPipeData hpd = new ClientPipeData(); + hpd.ChannelInfo = ChannelInfo; + + if (ChannelInfo.RequiresKeyExchange()) + { + df = new DiffieHellman(); + hpd.DHPublic = df.GetPublicKey(); + } + return hpd; + } + protected override void Log(LogType type,string log) + { + string prefix = $"[PipeState]: "; + prefix += isInitiator ? "A: " : "B: "; + base.Log(type,prefix + log); + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientRoomState.cs b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientRoomState.cs new file mode 100644 index 0000000..aa48fa1 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientRoomState.cs @@ -0,0 +1,67 @@ +using NetworkLibrary.DistributedP2P.Components; +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.Client.StateManagement +{ + internal class ClientRoomState : ConversationStateBase + { + private readonly IDistributedConnection connection; + + public ClientRoomState(Guid stateId, int timeout,IDistributedConnection connection, ILogger logger) : base(stateId, timeout, logger) + { + this.connection = connection; + } + + public void Start(string roomName,string passWord) + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.RequestCreateOrJointRoom; + msg.KeyValuePairs = new Dictionary(); + + msg.KeyValuePairs["RoomName"] = roomName; + msg.KeyValuePairs["Pass"] = passWord; + + connection.SendAsyncMessage(msg); + } + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.ResponseCreateOrJointRoom: + HandleResponse(message); + break; + } + } + + private void HandleResponse(MessageEnvelope message) + { + string succesStr = message.KeyValuePairs["Status"]; + bool success = bool.Parse(succesStr); + if (success) + { + string token = message.KeyValuePairs["Token"]; + ExchangeToken(token); + } + else + { + Completed(false); + } + + } + + private void ExchangeToken(string token) + { + TokenExchangeComplete(); + } + + private void TokenExchangeComplete() + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.JoinedRoom; + connection.SendAsyncMessage(msg); + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientSequentialTcpHolepunchState.cs b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientSequentialTcpHolepunchState.cs new file mode 100644 index 0000000..e2b9f02 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientSequentialTcpHolepunchState.cs @@ -0,0 +1,549 @@ +using NetworkLibrary.Components.Crypto.DiffieHellman; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Client.StateManagement +{ + class ClientHolepunchData + { + public ChannelInfo ChannelInfo { get; set; } + public EndpointTransferMessage Endpoints { get; set; } + public byte[] DHPublic; + } + + internal class ClientSequentialTcpHolepunchState : ConversationStateBase + { + private readonly Guid destId; + private readonly IDistributedConnection connection; + private readonly EndpointData serverEndpoint; + private readonly EndpointData discoveryServerEp; + private bool isInitiator; + public Socket Socket; + public IPEndPoint SuccesfulEndpoint; + + private DiffieHellman df = new DiffieHellman(); + private byte[] othersPublicKey; + public byte[] SharedSecret; + public ChannelInfo ChannelInfo; + private Socket listeningSocket; + private Socket acceptedSocket; + private Socket connectedSocket; + private int established = 0; + private int connected = 0; + private int accepted = 0; + + int swapCnt = 0; + + bool isListening = false; + List localEndpoints = new List(); + + private IPEndPoint selfRemoteEp; + private IPEndPoint selfLocalEp = new IPEndPoint(IPAddress.Any, 0); + private EndpointData publicEndpointToConnect; + + int conditionCount = 0; + + + private bool IsEstablished => Interlocked.CompareExchange(ref established, 0, 0) == 1; + public ClientSequentialTcpHolepunchState(Guid stateId, Guid destId, IDistributedConnection connection, EndpointData serverEndpoint, EndpointData discoveryServerEp, ChannelInfo info, ILogger logger) : base(stateId, 10000, logger) + { + this.destId = destId; + this.connection = connection; + this.serverEndpoint = serverEndpoint; + this.discoveryServerEp = discoveryServerEp; + this.ChannelInfo = info; + } + + //the initiator + public async void Start() + { + try + { + + + isInitiator = true; + Log(LogType.Debug, StateId.ToString()); + + await BindPort().ConfigureAwait(false); + Log(LogType.Debug, $"Local port {selfLocalEp.Port} Remote port {selfRemoteEp.Port}"); + + var msg = CreateEnvelope(); + msg.To = destId; + msg.Header = InternalConstants.RequestSequentialHolepunchTcp; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeHolepunchData(stream, GetHpData()); + + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + catch (Exception e) + { + Log(LogType.Exception, e.Message + "\n" + e.StackTrace); + Cancel(); + } + } + + + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.RequestSequentialHolepunchTcp: + HandleRemoteHpRequest(message); + break; + + case InternalConstants.StartHP: + StartHolepunchRoutine(message); + break; + + case InternalConstants.PunchSuccesAck: + HandleRemoteSucces(message); + break; + case InternalConstants.PunchFailAck: + HandleFailure(); + break; + case InternalConstants.PunchSwap: + Swap(); + break; + } + } + + // the destination peer of hp + private async void HandleRemoteHpRequest(MessageEnvelope message) + { + try + { + Log(LogType.Debug, StateId.ToString()); + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + ChannelInfo = hpData.ChannelInfo; + await BindPort().ConfigureAwait(false); + Log(LogType.Debug, $"Local port {selfLocalEp.Port} Remote port {selfRemoteEp.Port}"); + + StartTcpListener(); + Log(LogType.Debug, "listening"); + + var msg = CreateEnvelope(); + msg.To = destId; + msg.Header = InternalConstants.AckRequestHolepunchTcp; + + var hpd = GetHpData(); + hpd.ChannelInfo = null; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeHolepunchData(stream, hpd); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + + connection.SendAsyncMessage(msg); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + catch (Exception e) + { + Log(LogType.Exception, e.Message + "\n" + e.StackTrace); + Cancel(); + } + + } + + private void StartHolepunchRoutine(MessageEnvelope message) + { + try + { + + if (IsCompleted()) return; + int offs = message.PayloadOffset; + + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + var epMsg = hpData.Endpoints; + othersPublicKey = hpData.DHPublic; + localEndpoints = epMsg.LocalEndpoints; + + bool useServerIp = IPHelper.IsZero(epMsg.IpRemote); + publicEndpointToConnect = new EndpointData() { Ip = useServerIp ? serverEndpoint.Ip : epMsg.IpRemote, Port = epMsg.PortRemote }; + + AddompletionCondition(); + if (IsCompleted()) return; + + if (!isListening) + { + TryPunch(); + } + } + catch (Exception e) + { + if (!IsCompleted()) + { + Log(LogType.Exception, e.Message + "\n" + e.StackTrace); + Cancel(); + } + } + + } + + + private async void TryPunch() + { + try + { + if (localEndpoints.Count > 0) + { + foreach (EndpointData localEp in localEndpoints) + { + if (await TryConnect(localEp, 600).ConfigureAwait(false)) + return; + + if (IsCompleted()) return; + + } + } + + for (int i = 0; i < 1; i++) + { + if (await TryConnect(publicEndpointToConnect, (2000)).ConfigureAwait(false)) + return; + + if (IsCompleted()) return; + + } + } + catch { } + finally + { + if (Interlocked.CompareExchange(ref established, 0, 0) == 0) + SwapAndNotify(); + } + } + + //this one only when we tried to connect and failed + // so we will listen only + private void SwapAndNotify() + { + if (IsCompleted()) return; + + + int cnt = 0; + while (!isListening) + { + try + { + if (IsCompleted()) return; + Swap(); + if (IsCompleted()) return; + } + catch + { + Thread.Sleep(200); + } + + cnt++; + if (cnt > 10) + break; + + } + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSwap; + connection.SendAsyncMessage(msg); + } + + private void Swap() + { + if (swapCnt++ > 3) + { + Log(LogType.Debug, "Max attempts reached"); + Cancel(); + return; + } + + if (IsCompleted()) return; + + if (isListening) + { + Log(LogType.Debug, "Swapping to Sender"); + StopListener(); + ThreadPool.UnsafeQueueUserWorkItem(_ => TryPunch(), null); + } + else + { + Log(LogType.Debug, "Swapping to Listener"); + StartTcpListener(); + } + } + + private async Task TryConnect(EndpointData endpoint, int timeoutMs = 600) + { + Socket connectSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + try + { + Log(LogType.Debug,"Connecting to " + endpoint.ToIpEndpoint().ToString()); + connectSocket.Bind(selfLocalEp); + + var connectTask = connectSocket.ConnectAsync(endpoint.ToIpEndpoint()); + var timeoutTask = Task.Delay(timeoutMs); + + if (await Task.WhenAny(connectTask, timeoutTask).ConfigureAwait(false) == connectTask) + { + if (connectTask.IsFaulted) + { + throw connectTask.Exception; + } + HandleConnectedSocket(connectSocket); + Log(LogType.Debug, $"Successfully connected to {endpoint.ToIpEndpoint()}"); + return true; + + } + else + { + Log(LogType.Debug, $"Connection to {endpoint.ToIpEndpoint()} timed out after {timeoutMs}ms"); + connectSocket.Close(); + connectSocket.Dispose(); + return false; + } + } + catch (Exception ex) + { + Log(LogType.Debug, $"Connect attempt to {endpoint.ToIpEndpoint()} failed: {ex.Message}"); + connectSocket.Close(); + return false; + } + } + + private async Task BindPort() + { + var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + clientSocket.Bind(selfLocalEp); + selfLocalEp = (IPEndPoint)clientSocket.LocalEndPoint; + + var remoteEp = await EndpointDiscoveryClient.GetTcpPublicEndpoint(clientSocket, discoveryServerEp.ToIpEndpoint(), 5000).ConfigureAwait(false); + if (remoteEp == null) + { + Log(LogType.Warning, "Failed to get public endpoint"); + remoteEp = new EndpointData("0.0.0.0", selfLocalEp.Port); + } + + selfRemoteEp = remoteEp.ToIpEndpoint(); + + try + { + clientSocket?.Close(); + clientSocket?.Dispose(); + clientSocket = null; + } + catch { } + + } + + private int StartTcpListener() + { + listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listeningSocket.SendBufferSize = 12800000; + listeningSocket.ReceiveBufferSize = 12800000; + + listeningSocket.Bind(selfLocalEp); + selfLocalEp = (IPEndPoint)listeningSocket.LocalEndPoint; + + Listen(); + isListening = true; + return ((IPEndPoint)listeningSocket.LocalEndPoint).Port; + } + + private void Listen() + { + listeningSocket.Listen(1); + listeningSocket.BeginAccept(new AsyncCallback(AcceptCallback), listeningSocket); + } + + private void AcceptCallback(IAsyncResult ar) + { + try + { + Socket listener = (Socket)ar.AsyncState; + Socket handler = listener.EndAccept(ar); + + HandleAcceptedSocket(handler); + } + catch (Exception e) + { + Log(LogType.Debug, "Failed Accept: " + e.Message); + } + + } + + private void StopListener() + { + try + { + isListening = false; + listeningSocket?.Close(); + listeningSocket?.Dispose(); + listeningSocket = null; + } + catch { } + } + + // In this case one or the other + + private void HandleConnectedSocket(Socket socket) + { + if (IsCompleted()) return; + + Interlocked.Exchange(ref established, 1); + if (Interlocked.Exchange(ref connected, 1) == 1) + return; + + connectedSocket = socket; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSucces; + msg.KeyValuePairs = new Dictionary(); + msg.KeyValuePairs["Status"] = "Connected"; + connection.SendAsyncMessage(msg); + } + + private void HandleAcceptedSocket(Socket socket) + { + if (IsCompleted()) return; + + + Interlocked.Exchange(ref established, 1); + if (Interlocked.Exchange(ref accepted, 1) == 1) + return; + + Log(LogType.Debug,$"Successfully accepted {(IPEndPoint)socket.RemoteEndPoint}"); + acceptedSocket = socket; + + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSucces; + msg.KeyValuePairs = new Dictionary(); + msg.KeyValuePairs["Status"] = "Accepted"; + connection.SendAsyncMessage(msg); + } + + + private void HandleFailure() + { + Log(LogType.Warning, "Failed Punch"); + Completed(false); + } + + private void HandleRemoteSucces(MessageEnvelope message) + { + AddompletionCondition(); + } + private void AddompletionCondition() + { + if (Interlocked.Increment(ref conditionCount) == 2) + { + if (ChannelInfo.RequiresKeyExchange()) + SharedSecret = df.CalculateSharedSecret(othersPublicKey); + + if (connectedSocket != null) + { + Socket = connectedSocket; + } + else + { + Socket = acceptedSocket; + } + + if (Socket == null) + { + Log(LogType.Warning, "Failed to get socket"); + Completed(false); + return; + } + SuccesfulEndpoint = (IPEndPoint)Socket.RemoteEndPoint; + Log(LogType.Debug, "Punched"); + Completed(true); + } + } + + + public override void Cancel() + { + lock (cancellationMutex) + { + if (!IsCompleted()) + { + Log(LogType.Debug, "Cancelled"); + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchFail; + connection.SendAsyncMessage(msg); + Completed(false); + } + } + + } + + protected override void Completed(bool succes) + { + base.Completed(succes); + + try + { + if (!IsSuccesful) + { + acceptedSocket?.Close(); + acceptedSocket?.Dispose(); + connectedSocket?.Close(); + connectedSocket?.Dispose(); + } + + listeningSocket?.Close(); + listeningSocket?.Dispose(); + } + catch { } + + } + + protected override void Log(LogType type,string log) + { + string prefix =$"[TcpHolepunchState]: "; + prefix += isInitiator ? "A: " : "B: "; + base.Log(type,prefix + log); + } + + private ClientHolepunchData GetHpData() + { + ClientHolepunchData hpd = new ClientHolepunchData(); + hpd.ChannelInfo = ChannelInfo; + + var epm = new EndpointTransferMessage(); + var pub = new EndpointData(selfRemoteEp); + epm.IpRemote = pub.Ip; + epm.PortRemote = pub.Port; + + var localIps = IPHelper.GetLocalIPAddresses4(); + int localPort = selfLocalEp.Port; + foreach (var ip in localIps) + { + epm.LocalEndpoints.Add(new EndpointData(ip, localPort)); + } + + hpd.Endpoints = epm; + + if (ChannelInfo.RequiresKeyExchange()) + { + hpd.DHPublic = df.GetPublicKey(); + } + return hpd; + } + } + + +} diff --git a/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientSimultaneousTcpHolepunchState.cs b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientSimultaneousTcpHolepunchState.cs new file mode 100644 index 0000000..e912e32 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientSimultaneousTcpHolepunchState.cs @@ -0,0 +1,436 @@ +using NetworkLibrary.Components.Crypto.DiffieHellman; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using NetworkLibrary.Utils; + +namespace NetworkLibrary.DistributedP2P.Client.StateManagement +{ + internal class ClientSimultaneousTcpHolepunchState : ConversationStateBase + { + private readonly Guid destId; + private readonly IDistributedConnection connection; + private readonly EndpointData serverEndpoint; + private readonly EndpointData discoveryServerEp; + private bool isInitiator; + public Socket Socket; + public IPEndPoint SuccesfulEndpoint; + + private DiffieHellman df = new DiffieHellman(); + private byte[] otherPublicKey; + List localEndpoints = new List(); + + public byte[] SharedSecret; + public ChannelInfo ChannelInfo; + private int localPort; + private Socket listeningSocket; + private Socket acceptedSocket; + private Socket connectedSocket; + private int established = 0; + private int connected = 0; + private int accepted = 0; + + private IPEndPoint selfRemoteEp; + private IPEndPoint selfLocalEp = new IPEndPoint(IPAddress.Any, 0); + private EndpointData publicEndpointToConnect; + + + private bool IsEstablished => Interlocked.CompareExchange(ref established, 0, 0) == 1; + public ClientSimultaneousTcpHolepunchState(Guid stateId, Guid destId, IDistributedConnection connection, EndpointData serverEndpoint, EndpointData discoveryServerEp, ChannelInfo info, ILogger logger) : base(stateId, 5000, logger) + { + this.destId = destId; + this.connection = connection; + this.serverEndpoint = serverEndpoint; + this.discoveryServerEp = discoveryServerEp; + this.ChannelInfo = info; + } + + //the initiator + public async void Start() + { + try + { + + isInitiator = true; + Log(LogType.Debug, StateId.ToString()); + + await BindPort().ConfigureAwait(false); + Log(LogType.Debug, $"Local port {selfLocalEp.Port} Remote port {selfRemoteEp.Port}"); + + StartListening(); + Log(LogType.Debug, "listening"); + + var msg = CreateEnvelope(); + msg.To = destId; + msg.Header = InternalConstants.RequestSimultaneousHolepunchTcp; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeHolepunchData(stream, GetHpData()); + + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + catch (Exception e) + { + Log(LogType.Exception, e.Message + "\n" + e.StackTrace); + Cancel(); + } + + } + + + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.RequestSimultaneousHolepunchTcp: + HandleRemoteHpRequest(message); + break; + + case InternalConstants.StartHP: + message.LockBytes(); + ThreadPool.UnsafeQueueUserWorkItem((s) => StartHolepunchRoutine(message), null); + break; + + case InternalConstants.PunchSuccesAck: + HandleRemoteSucces(message); + break; + case InternalConstants.PunchFailAck: + HandleFailure(); + break; + } + } + + // the destination peer of hp + private async void HandleRemoteHpRequest(MessageEnvelope message) + { + try + { + + + Log(LogType.Debug, StateId.ToString()); + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + ChannelInfo = hpData.ChannelInfo; + await BindPort(); + Log(LogType.Debug, $"Local port {selfLocalEp.Port} Remote port {selfRemoteEp.Port}"); + + StartListening(); + Log(LogType.Debug, "listening"); + + var msg = CreateEnvelope(); + msg.To = destId; + msg.Header = InternalConstants.AckRequestHolepunchTcp; + + var hpd = GetHpData(); + hpd.ChannelInfo = null; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeHolepunchData(stream, hpd); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + + connection.SendAsyncMessage(msg); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + catch (Exception e) + { + Log(LogType.Exception, e.Message + "\n" + e.StackTrace); + Cancel(); + } + + + } + + private async Task BindPort() + { + var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + clientSocket.Bind(selfLocalEp); + selfLocalEp = (IPEndPoint)clientSocket.LocalEndPoint; + + var remoteEp = await EndpointDiscoveryClient.GetTcpPublicEndpoint(clientSocket, discoveryServerEp.ToIpEndpoint(), 5000).ConfigureAwait(false); + if (remoteEp == null) + { + Log(LogType.Warning, "Failed to get public endpoint"); + remoteEp = new EndpointData("0.0.0.0", selfLocalEp.Port); + } + + selfRemoteEp = remoteEp.ToIpEndpoint(); + + try + { + clientSocket?.Close(); + clientSocket?.Dispose(); + clientSocket = null; + } + catch { } + + } + + private void StartHolepunchRoutine(MessageEnvelope message) + { + try + { + + + if (IsCompleted()) return; + int offs = message.PayloadOffset; + + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + var epMsg = hpData.Endpoints; + otherPublicKey = hpData.DHPublic; + localEndpoints = epMsg.LocalEndpoints; + + bool useServerIp = IPHelper.IsZero(epMsg.IpRemote); + publicEndpointToConnect = new EndpointData() { Ip = useServerIp ? serverEndpoint.Ip : epMsg.IpRemote, Port = epMsg.PortRemote }; + var time = double.Parse(message.KeyValuePairs["Time"]); + + + // if there are local endpoints to test + if (epMsg.LocalEndpoints.Count > 0) + { + foreach (EndpointData localEp in epMsg.LocalEndpoints) + { + if (TryConnect(localEp, 500)) + return; + if (IsEstablished) return; + } + } + + if (IsEstablished) return; + + var now = connection.GetTime(); + var delay = time - now; + + Log(LogType.Debug, "Delay: " + delay.ToString() + "ms"); + + PreciseTimeAwaiter.Wait(delay); + if (IsEstablished) return; + + for (int i = 0; i < 4; i++) + { + if (TryConnect(publicEndpointToConnect, (2000))) + return; + if (IsEstablished) return; + } + } + catch (Exception e) + { + if (!IsCompleted()) + { + Log(LogType.Exception, e.Message + "\n" + e.StackTrace); + TimedOut(); + } + } + + } + + + private bool TryConnect(EndpointData endpoint, int timeoutMs = 600) + { + Socket connectSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + try + { + Log(LogType.Debug, "Connecting to " + endpoint.ToIpEndpoint().ToString()); + connectSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + connectSocket.Bind(new IPEndPoint(IPAddress.Any, localPort)); + + var connectTask = connectSocket.ConnectAsync(endpoint.ToIpEndpoint()); + var timeoutTask = Task.Delay(timeoutMs); + + if (Task.WhenAny(connectTask, timeoutTask).GetAwaiter().GetResult() == connectTask) + { + + connectSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); + HandleConnectedSocket(connectSocket); + Log(LogType.Debug, $"Successfully connected to {endpoint.ToIpEndpoint()}"); + return true; + + } + else + { + Log(LogType.Debug, $"Connection to {endpoint.ToIpEndpoint()} timed out after {timeoutMs}ms"); + connectSocket.Close(); + return false; + } + } + catch (Exception ex) + { + Log(LogType.Debug, $"Connect attempt to {endpoint.ToIpEndpoint()} failed: {ex.Message}"); + connectSocket.Close(); + return false; + } + } + + + private int StartListening() + { + listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listeningSocket.SendBufferSize = 12800000; + listeningSocket.ReceiveBufferSize = 12800000; + listeningSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + listeningSocket.Bind(selfLocalEp); + + Listen(); + + return ((IPEndPoint)listeningSocket.LocalEndPoint).Port; + } + + private void Listen() + { + listeningSocket.Listen(1); + listeningSocket.BeginAccept(new AsyncCallback(AcceptCallback), listeningSocket); + } + + private void AcceptCallback(IAsyncResult ar) + { + try + { + Socket listener = (Socket)ar.AsyncState; + Socket handler = listener.EndAccept(ar); + + HandleAcceptedSocket(handler); + } + catch (Exception e) + { + Log(LogType.Debug, "Failed Accept: " + e.Message); + } + + } + + private void HandleConnectedSocket(Socket socket) + { + Interlocked.Exchange(ref established, 1); + if (Interlocked.Exchange(ref connected, 1) == 1) + return; + + connectedSocket = socket; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSucces; + msg.KeyValuePairs = new Dictionary(); + msg.KeyValuePairs["Status"] = "Connected"; + connection.SendAsyncMessage(msg); + } + + private void HandleAcceptedSocket(Socket socket) + { + Interlocked.Exchange(ref established, 1); + if (Interlocked.Exchange(ref accepted, 1) == 1) + return; + + Log(LogType.Debug, $"Successfully accepted {(IPEndPoint)socket.RemoteEndPoint}"); + acceptedSocket = socket; + + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSucces; + msg.KeyValuePairs = new Dictionary(); + msg.KeyValuePairs["Status"] = "Accepted"; + connection.SendAsyncMessage(msg); + } + + private void TimedOut() + { + Log(LogType.Debug, "Timed out"); + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchFail; + connection.SendAsyncMessage(msg); + Cancel(); + } + + private void HandleFailure() + { + Log(LogType.Debug, "Failed Punch"); + Completed(false); + } + + private void HandleRemoteSucces(MessageEnvelope message) + { + string use = message.KeyValuePairs["Use"]; + if (ChannelInfo.RequiresKeyExchange()) + SharedSecret = df.CalculateSharedSecret(otherPublicKey); + + if (use == "Connected") + { + Socket = connectedSocket; + } + else + { + Socket = acceptedSocket; + } + + SuccesfulEndpoint = (IPEndPoint)Socket.RemoteEndPoint; + Log(LogType.Debug,"Punched"); + Completed(true); + } + + protected override void Completed(bool succes) + { + base.Completed(succes); + + try + { + if (!IsSuccesful) + { + acceptedSocket?.Close(); + acceptedSocket?.Dispose(); + connectedSocket?.Close(); + connectedSocket?.Dispose(); + } + + listeningSocket?.Close(); + listeningSocket?.Dispose(); + } + catch { } + + } + + protected override void Log(LogType type,string log) + { + string prefix = $"[TcpHolepunchState]: "; + prefix += isInitiator ? "A: " : "B: "; + base.Log(type,prefix + log); + } + + private ClientHolepunchData GetHpData() + { + ClientHolepunchData hpd = new ClientHolepunchData(); + hpd.ChannelInfo = ChannelInfo; + + var epm = new EndpointTransferMessage(); + var pub = new EndpointData(selfRemoteEp); + epm.IpRemote = pub.Ip; + epm.PortRemote = pub.Port; + + var localIps = IPHelper.GetLocalIPAddresses4(); + int localPort = selfLocalEp.Port; + foreach (var ip in localIps) + { + epm.LocalEndpoints.Add(new EndpointData(ip, localPort)); + } + + hpd.Endpoints = epm; + + if (ChannelInfo.RequiresKeyExchange()) + { + hpd.DHPublic = df.GetPublicKey(); + } + return hpd; + } + + } + + +} diff --git a/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientUdpHolepunchState.cs b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientUdpHolepunchState.cs new file mode 100644 index 0000000..e629cfd --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Client/StateManagement/ClientUdpHolepunchState.cs @@ -0,0 +1,460 @@ +using NetworkLibrary.Components; +using NetworkLibrary.Components.Crypto.DiffieHellman; +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Client.StateManagement +{ + //Todo 0.0.0.0 means server ip! + internal class ClientUdpHolepunchState : ConversationStateBase + { + private readonly Guid destId; + private readonly IDistributedConnection connection; + private readonly EndpointData serverEndpoint; + private readonly EndpointData discoveryServerendPoint; + public Socket Socket; + private bool isInitiator; + + public IPEndPoint SuccesfulEndpoint; + + private DiffieHellman df = new DiffieHellman(); + private byte[] otherPublicKey; + public byte[] SharedSecret; + public ChannelInfo ChannelInfo; + private IPEndPoint selfRemoteEp; + private IPEndPoint selfLocalEp; + + private int conditionCount = 0; + + public ClientUdpHolepunchState(Guid stateId, + Guid destId, + IDistributedConnection connection, + EndpointData serverEndpoint, + EndpointData discoveryServerendPoint, + ChannelInfo info, + ILogger logger) : base(stateId, 20000, logger) + { + this.destId = destId; + this.connection = connection; + this.serverEndpoint = serverEndpoint; + this.discoveryServerendPoint = discoveryServerendPoint; + this.ChannelInfo = info; + } + + //the initiator + public async void Start() + { + try + { + isInitiator = true; + Log(LogType.Debug, StateId.ToString()); + + await StartUdpSocket().ConfigureAwait(false); + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.RequestHolepunchUdp; + msg.To = destId; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeHolepunchData(stream, GetHpData()); + + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + catch (Exception ex) + { + OnError(ex.Message + "\n" + ex.StackTrace); + } + } + + + + public override void HandleMessage(MessageEnvelope message) + { + try + { + switch (message.Header) + { + case InternalConstants.RequestHolepunchUdp: + HandleRemoteHpRequest(message); + break; + + case InternalConstants.StartHP: + StartHolepunchRoutine(message); + break; + + case InternalConstants.PunchSuccesAck: + HandleRemoteSucces(message); + break; + case InternalConstants.PunchFailAck: + HandleFailure(); + break; + } + } + catch (Exception ex) + { + OnError(ex.Message + "\n" + ex.StackTrace); + } + + } + + // the destination peer of hp + private async void HandleRemoteHpRequest(MessageEnvelope message) + { + try + { + Log(LogType.Debug, StateId.ToString()); + + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + ChannelInfo = hpData.ChannelInfo; + + await StartUdpSocket().ConfigureAwait(false); + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.AckRequestHolepunchUdp; + msg.To = destId; + + var hpd = GetHpData(); + hpd.ChannelInfo = null; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeHolepunchData(stream, hpd); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + + connection.SendAsyncMessage(msg); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + catch (Exception ex) + { + OnError(ex.Message + "\n" + ex.StackTrace); + } + + } + + private async Task StartUdpSocket() + { + + Socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + Socket.SendBufferSize = 12800000; + Socket.ReceiveBufferSize = 12800000; + Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + + Socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + + selfLocalEp = (IPEndPoint)Socket.LocalEndPoint; + + var remoteEp = new EndpointData("0.0.0.0", selfLocalEp.Port); + + int retryCount = 0; + while (true) + { + remoteEp = await EndpointDiscoveryClient.GetUdpPublicEndpoint(Socket, discoveryServerendPoint.ToIpEndpoint(), 2000).ConfigureAwait(false); + if (remoteEp == null) + { + if (retryCount++ < 2) + continue; + Log(LogType.Warning, "Failed to get public endpoint"); + remoteEp = new EndpointData("0.0.0.0", selfLocalEp.Port); + } + break; + } + + selfRemoteEp = remoteEp.ToIpEndpoint(); + + Receive(); + + + } + + private async void StartHolepunchRoutine(MessageEnvelope message) + { + try + { + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + var epMsg = hpData.Endpoints; + otherPublicKey = hpData.DHPublic; + + var time = double.Parse(message.KeyValuePairs["Time"]); + SignalCompletionCondition(); + + if (epMsg.LocalEndpoints.Count > 0) + { + //var now0 = connection.GetTime(); + //var delay0 = (time - now0) / 4; + + foreach (EndpointData localEp in epMsg.LocalEndpoints) + { + for (int i = 0; i < 2; i++) + { + TryPunch(localEp, MessageFlags.HP); + await Task.Delay(100).ConfigureAwait(false); + if (IsCompleted()) return; + } + } + } + + + if (IsCompleted()) return; + + // use server ip, peer is on same network as server + bool useServerIp = IPHelper.IsZero(epMsg.IpRemote); + EndpointData publicEp = new EndpointData() { Ip = useServerIp ? serverEndpoint.Ip : epMsg.IpRemote, Port = epMsg.PortRemote }; + + + var now = connection.GetTime(); + var delay = time - now; + if (delay > 500) + delay = 0; + + Log(LogType.Debug, "Delay: " + delay.ToString() + "ms"); + if (delay > 0) + await Task.Delay((int)delay).ConfigureAwait(false); + if (IsCompleted()) return; + + for (int i = 0; i < 8; i++) + { + TryPunch(publicEp, MessageFlags.HP); + await Task.Delay(20 + (20 * i * i)).ConfigureAwait(false); + if (IsCompleted()) return; + } + } + catch (Exception ex) + { + if (!IsCompleted()) + { + OnError(ex.Message + "\n" + ex.StackTrace); + } + } + + + } + + private void TryPunch(EndpointData ep, MessageFlags flag) + { + var ipep = ep.ToIpEndpoint(); + TryPunch(ipep, flag); + } + private object m = new object(); + PooledMemoryStream stream = new PooledMemoryStream(); + + private void TryPunch(IPEndPoint ep, MessageFlags flag) + { + lock (m) + { + + Log(LogType.Debug, $"Sending {flag.ToString()}To " + ep.ToString()); + + stream.Position = 0; + stream.WriteByte((byte)flag); + var epd = new EndpointData(ep); + KnownTypeSerializer.SerializeEndpointData(stream, epd); + + Socket.SendTo(stream.GetBuffer(), stream.Position32, SocketFlags.None, ep); + } + + } + + private async void Receive() + { + var buffer = BufferPool.RentBuffer(64000); + int receivedOnce = 0; + int receivedAck = 0; + while (true) + { + try + { + var remoteEP = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + var receiveTask = Socket.ReceiveFromAsync(new ArraySegment(buffer), SocketFlags.None, remoteEP); + + var completedTask = await Task.WhenAny(receiveTask, Task.Delay(base.timeout)).ConfigureAwait(false); + if (completedTask == receiveTask) + { + SocketReceiveFromResult received = await receiveTask; + + if (buffer[0] == (byte)MessageFlags.HP) + { + // this must be only once + if (Interlocked.CompareExchange(ref receivedOnce, 1, 0) == 0) + { + var ipep = (IPEndPoint)received.RemoteEndPoint; + Log(LogType.Debug, "[-]Received 0xFF from " + ipep.ToString()); + TryPunch((IPEndPoint)received.RemoteEndPoint, MessageFlags.HPAck); + await Task.Delay(500).ConfigureAwait(false); + if (IsCompleted()) return; + TryPunch((IPEndPoint)received.RemoteEndPoint, MessageFlags.HPAck); + await Task.Delay(500).ConfigureAwait(false); + if (IsCompleted()) return; + TryPunch((IPEndPoint)received.RemoteEndPoint, MessageFlags.HPAck); + + } + + } + else if (buffer[0] == (byte)MessageFlags.HPAck) + { + Log(LogType.Debug, "[+]Received 0x0F From " + ((IPEndPoint)received.RemoteEndPoint).ToString()); + + if (Interlocked.CompareExchange(ref receivedAck, 1, 0) == 0) + { + TryPunch((IPEndPoint)received.RemoteEndPoint, MessageFlags.HPAck); + ReceivedBidirectional(received.RemoteEndPoint); + } + return; + } + else + { + Log(LogType.Warning, "Cancel1"); + Cancel(); + return; + } + } + else + { + TimedOut(); + return; + } + } + catch (Exception e) + { + Log(LogType.Error, "ERROR" + e.Message); + Cancel(); + return; + } + finally + { + BufferPool.ReturnBuffer(buffer); + } + } + + } + // only called once when succesfuly received. + private void ReceivedBidirectional(EndPoint remoteEndPoint) + { + + if (remoteEndPoint == null) return; + + var ipep = (IPEndPoint)remoteEndPoint; + + if (Interlocked.CompareExchange(ref SuccesfulEndpoint, ipep, null) != null) + return; + + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSucces; + connection.SendAsyncMessage(msg); + + SignalCompletionCondition(); + } + + private void TimedOut() + { + Log(LogType.Warning, "Timed out"); + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchFail; + connection.SendAsyncMessage(msg); + Cancel(); + } + + private void OnError(string error) + { + Log(LogType.Warning, "Exception :" + error); + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchFail; + connection.SendAsyncMessage(msg); + Cancel(); + } + + + private void HandleFailure() + { + Log(LogType.Warning, "Failed Punch"); + + Completed(false); + } + // Need consesus! + private void HandleRemoteSucces(MessageEnvelope message) + { + SignalCompletionCondition(); + } + + private void SignalCompletionCondition() + { + if (Interlocked.Increment(ref conditionCount) == 3) + { + if (ChannelInfo.RequiresKeyExchange()) + SharedSecret = df.CalculateSharedSecret(otherPublicKey); + + if (SuccesfulEndpoint == null) + throw new Exception("Endpont is null"); + + Log(LogType.Debug, "Punched"); + Completed(true); + } + + } + + protected override void Completed(bool succes) + { + base.Completed(succes); + if (!IsSuccesful) + { + try + { + Socket?.Close(); + Socket?.Dispose(); + } + catch { } + } + } + + protected override void Log(LogType type, string log) + { + string prefix = $"[UdpHolepunchState]: "; + prefix += isInitiator ? "A: " : "B: "; + base.Log(type, prefix + log); + } + + + private ClientHolepunchData GetHpData() + { + ClientHolepunchData hpd = new ClientHolepunchData(); + hpd.ChannelInfo = ChannelInfo; + + var epm = new EndpointTransferMessage(); + var pub = new EndpointData(selfRemoteEp); + epm.IpRemote = pub.Ip; + epm.PortRemote = pub.Port; + + var localIps = IPHelper.GetLocalIPAddresses4(); + int localPort = selfLocalEp.Port; + foreach (var ip in localIps) + { + epm.LocalEndpoints.Add(new EndpointData(ip, localPort)); + } + + hpd.Endpoints = epm; + + + if (ChannelInfo.RequiresKeyExchange()) + { + hpd.DHPublic = df.GetPublicKey(); + } + return hpd; + } + + } + + +} diff --git a/NetworkLibrary/DistributedP2P/Components/ConversationStateBase.cs b/NetworkLibrary/DistributedP2P/Components/ConversationStateBase.cs new file mode 100644 index 0000000..5f4359c --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/ConversationStateBase.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal abstract class ConversationStateBase:IConversationState + { + public Guid StateId { get; private set; } + + public bool IsSuccesful { get; private set; } + + public event Action OnComplete; + + public string ErrorMessage { get; protected set; } + + protected readonly object cancellationMutex = new object(); + protected readonly int timeout; + private readonly ILogger logger; + private TaskCompletionSource Completion; + private int isComplete = 0; + + public ConversationStateBase(Guid stateId, int timeout, ILogger logger) + { + this.StateId = stateId; + this.timeout = timeout; + this.logger = logger; + Completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + if (timeout > 0) + { + TimerService.RegisterTimer(stateId, timeout, OnTimeOut); + } + } + + private void OnTimeOut() + { + if (!IsCompleted()) + { + Cancel(); + } + } + + public abstract void HandleMessage(MessageEnvelope message); + + public Task WaitCompletion() + { + return Completion.Task; + } + + public virtual void Cancel() + { + lock (cancellationMutex) + { + Completed(false); + } + + } + protected virtual MessageEnvelope CreateErrorMsg(string err) + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.Error; + msg.KeyValuePairs = new Dictionary + { + { "Error", err } + }; + return msg; + } + protected virtual MessageEnvelope CreateEnvelope() + { + MessageEnvelope msg = new MessageEnvelope(); + msg.MessageId = StateId; + msg.IsInternal = true; + return msg; + } + + protected bool IsCompleted() + { + return Interlocked.CompareExchange(ref isComplete, 0, 0) == 1; + } + + + protected virtual void Completed(bool succes) + { + if (Interlocked.CompareExchange(ref isComplete, 1, 0) == 0) + { + + IsSuccesful = succes; + OnComplete?.Invoke(this); + Completion.SetResult(this); + OnComplete = null; + } + } + + protected virtual void Log(LogType logType,string log) + { + logger?.Log(logType,log); + } + + + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/EndpointDiscoveryClient.cs b/NetworkLibrary/DistributedP2P/Components/EndpointDiscoveryClient.cs new file mode 100644 index 0000000..6aaf47f --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/EndpointDiscoveryClient.cs @@ -0,0 +1,78 @@ +using NetworkLibrary.P2P.Components.HolePunch; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class EndpointDiscoveryClient + { + static byte[] dummy = new byte[1]; + + public static async Task GetTcpPublicEndpoint(Socket socket, IPEndPoint whereToAsk, int timeoutMs) + { + var retrieveTask = GetPublicEndpointTcp(socket, whereToAsk); + var result = await Task.WhenAny(retrieveTask, Task.Delay(timeoutMs)).ConfigureAwait(false); + if (result == retrieveTask) + { + if (!retrieveTask.IsFaulted) + return await retrieveTask; + return null; + } + else + { + return null; + } + } + + public static async Task GetUdpPublicEndpoint(Socket socket, IPEndPoint whereToAsk, int timeoutMs) + { + var retrieveTask = GetPublicEndpointUdp(socket, whereToAsk); + var result = await Task.WhenAny(retrieveTask, Task.Delay(timeoutMs)).ConfigureAwait(false); + if (result == retrieveTask) + { + if (!retrieveTask.IsFaulted) + return retrieveTask.Result; + return null; + } + else + { + return null; + } + } + + + private static async Task GetPublicEndpointTcp(Socket socket, IPEndPoint whereToAsk) + { + + await socket.ConnectAsync(whereToAsk).ConfigureAwait(false); + var buff = BufferPool.RentBuffer(1024); + int received = await socket.ReceiveAsync(new ArraySegment(buff), SocketFlags.None).ConfigureAwait(false); + + + if (received == 0) + { + return null; + } + + var data = KnownTypeSerializer.DeserializeEndpointData(buff, 0); + BufferPool.ReturnBuffer(buff); + return data; + + } + + private static async Task GetPublicEndpointUdp(Socket socket, IPEndPoint whereToAsk) + { + await socket.SendToAsync(new ArraySegment(dummy), SocketFlags.None, whereToAsk).ConfigureAwait(false); + var buff = BufferPool.RentBuffer(1024); + + await socket.ReceiveFromAsync(new ArraySegment(buff), SocketFlags.None, whereToAsk).ConfigureAwait(false); + + var data = KnownTypeSerializer.DeserializeEndpointData(buff, 0); + BufferPool.ReturnBuffer(buff); + return data; + + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/EndpointDiscoveryServer.cs b/NetworkLibrary/DistributedP2P/Components/EndpointDiscoveryServer.cs new file mode 100644 index 0000000..e7d1eeb --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/EndpointDiscoveryServer.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using NetworkLibrary.DistributedP2P.Channels; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.TCP.Base; +using NetworkLibrary.Utils; +using NetworkLibrary.UDP; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class EndpointDiscoveryServer + { + + AsyncTcpServer tcpServer; + AsyncUdpServerLite udpServer; + public EndpointDiscoveryServer(int port) + { + tcpServer = new AsyncTcpServer(port); + tcpServer.OnClientAccepted += HandleTcpClient; + udpServer = new AsyncUdpServerLite(port); + udpServer.OnBytesRecieved += HandleUdpClient; + } + + private void HandleUdpClient(IPEndPoint remoteEp, byte[] bytes, int offset, int count) + { + try + { + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeEndpointData(stream, new EndpointData(remoteEp)); + udpServer.SendBytesToClient(remoteEp, stream.GetBuffer(), 0, stream.Position32); + } + catch { } + + } + + private void HandleTcpClient(Guid guid) + { + try + { + var remoteEp = tcpServer.GetSessionEndpoint(guid); + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + KnownTypeSerializer.SerializeEndpointData(stream, new EndpointData(remoteEp)); + tcpServer.SendBytesToClient(guid, stream.GetBuffer(), 0, stream.Position32); + + TimerService.RegisterTimer(guid, 1000, () => + { + tcpServer?.CloseSession(guid); + }); + } + catch { } + + } + + public void Start() + { + tcpServer.StartServer(); + udpServer.StartServer(); + } + + internal void Dispose() + { + try + { + tcpServer?.ShutdownServer(); + udpServer?.Dispose(); + } + catch { } + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/IConversationState.cs b/NetworkLibrary/DistributedP2P/Components/IConversationState.cs new file mode 100644 index 0000000..28fe927 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/IConversationState.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Components +{ + public interface IConversationState + { + Guid StateId { get; } + bool IsSuccesful { get; } + + void HandleMessage(MessageEnvelope message); + void Cancel(); + Task WaitCompletion(); + + event Action OnComplete; + + + } +} \ No newline at end of file diff --git a/NetworkLibrary/DistributedP2P/Components/IDistributedConnection.cs b/NetworkLibrary/DistributedP2P/Components/IDistributedConnection.cs new file mode 100644 index 0000000..c9d4d02 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/IDistributedConnection.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal interface IDistributedConnection : ITimeProvider + { + void SendAsyncMessage(MessageEnvelope msgs); + void SendAsyncMessage(Guid a, MessageEnvelope msgs); + Task SendMessageAndWaitResponse(Guid a, MessageEnvelope msg); + Task SendMessageAndWaitResponse(MessageEnvelope msg); + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/ILogger.cs b/NetworkLibrary/DistributedP2P/Components/ILogger.cs new file mode 100644 index 0000000..f2f74f4 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/ILogger.cs @@ -0,0 +1,14 @@ +using System; + +namespace NetworkLibrary.DistributedP2P.Components +{ + public interface ILogger + { + event Action LogAvailable; + + void Log(LogType logType, string log); + void Log(Exception ex); + void SetAllowedOptions(LogType allowedLogTypes); + string Stringify(LogData data); + } +} \ No newline at end of file diff --git a/NetworkLibrary/DistributedP2P/Components/IPHelper.cs b/NetworkLibrary/DistributedP2P/Components/IPHelper.cs new file mode 100644 index 0000000..75d465d --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/IPHelper.cs @@ -0,0 +1,340 @@ +using NetworkLibrary.DistributedP2P.Server; +using NetworkLibrary.P2P.Components.HolePunch; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class IPHelper + { + public static List GetLocalIPAddresses4() + { + List ipAddresses = new List(); + NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces(); + foreach (NetworkInterface ni in interfaces) + { + if (ni.OperationalStatus != OperationalStatus.Up || + ni.NetworkInterfaceType == NetworkInterfaceType.Loopback || + ni.Description.Contains("VMware") || + ni.Description.Contains("Hyper-V") || + ni.Description.Contains("VirtualBox") || + ni.Name.Contains("vEthernet")) + continue; + + IPInterfaceProperties ipProps = ni.GetIPProperties(); + foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses) + { + if (addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + ipAddresses.Add(addr.Address.ToString()); + } + } + } + return ipAddresses; + } + + // based on reserved private networks by RFC 1918 + public static bool IsPrivateIPAddress(IPEndPoint endpont) + { + IPAddress address = endpont.Address; + byte[] bytes = address.GetAddressBytes(); + + return IsPrivateIPAddress(bytes); + + } + + // based on reserved private networks by RFC 1918 + public static bool IsPrivateIPAddress(string ipAddress) + { + if (!IPAddress.TryParse(ipAddress, out IPAddress address)) + return false; + + if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + return false; + + byte[] bytes = address.GetAddressBytes(); + + return IsPrivateIPAddress(bytes); + } + + public static bool IsPrivateIPAddress(byte[] bytes) + { + // 10.0.0.0 - 10.255.255.255 (10/8 prefix) + if (bytes[0] == 10) + return true; + + // 172.16.0.0 - 172.31.255.255 (172.16/12 prefix) + if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) + return true; + + // 192.168.0.0 - 192.168.255.255 (192.168/16 prefix) + if (bytes[0] == 192 && bytes[1] == 168) + return true; + + // 169.254.0.0 - 169.254.255.255 (169.254/16 prefix, link-local) + if (bytes[0] == 169 && bytes[1] == 254) + return true; + + // 127.0.0.0 - 127.255.255.255 (127/8 prefix, loopback) + if (bytes[0] == 127) + return true; + + return false; + } + + public static void ExtractLocalIpsWithMatchingSubnet(List fromLocals, List ToLocals, out List toFrom, out List toTo) + { + // Get all possible subnet pairs between the two lists + var subnetPairs = from a in fromLocals + from b in ToLocals + let subnetA = GetSubnet(a) + let subnetB = GetSubnet(b) + where subnetA == subnetB + select new { a, b }; + + // Group by the matching subnets to maintain order + var matchedSubnets = subnetPairs + .GroupBy(pair => GetSubnet(pair.a)) + .OrderByDescending(g => g.Key) // Just to have consistent ordering + .SelectMany(g => g.Select(p => p)); + + // First add all IPs with matching subnets (ordered by subnet matches) + var orderedToFrom = new List(); + var orderedToTo = new List(); + + foreach (var pair in matchedSubnets) + { + if (!orderedToTo.Contains(pair.a)) + { + orderedToTo.Add(pair.a); + } + if (!orderedToFrom.Contains(pair.b)) + { + orderedToFrom.Add(pair.b); + } + } + + // Then add remaining IPs that didn't have matches + foreach (var ip in ToLocals) + { + if (!orderedToFrom.Contains(ip)) + { + orderedToFrom.Add(ip); + } + } + + foreach (var ip in fromLocals) + { + if (!orderedToTo.Contains(ip)) + { + orderedToTo.Add(ip); + } + } + + toFrom = orderedToFrom; + toTo = orderedToTo; + } + + // Helper method to get the first two octets (subnet /16) + private static string GetSubnet(string ip) + { + var parts = ip.Split('.'); + if (parts.Length >= 2) + { + return $"{parts[0]}.{parts[1]}"; + } + return ip; // fallback for invalid IPs (though shouldn't happen with valid IPs) + } + + public static bool IsZero(byte[] ipRemote) + { + if (ipRemote.Length != 4) + throw new InvalidOperationException("Ip must be 4 bytes"); + + unsafe + { + fixed (byte* ptr = ipRemote) + { + return *((int*)ptr) == 0; + } + } + } + + public static bool AreEqual(byte[] ip1, byte[] ip2) + { + + unsafe + { + fixed (byte* ptr1 = ip1) + fixed (byte* ptr2 = ip2) + { + return *((int*)ptr1) == *((int*)ptr2); + } + } + } + + public static void ObtainIpEndpoints(int fromPort, int toPort, ServerSession sesFrom, ServerSession sesTo, out EndpointTransferMessage FromNeedsToKnow, out EndpointTransferMessage ToNeedsToKnow) + { + FromNeedsToKnow = new EndpointTransferMessage(); + ToNeedsToKnow = new EndpointTransferMessage(); + + IPHelper.ExtractLocalIpsWithMatchingSubnet(sesFrom.ClientLocalIps, + sesTo.ClientLocalIps, + out List Locals_From_NeedsToKnow, + out List Locals_To_NeedsToKnow); + + + //They need to know locals when both peers have public IPs same(coming from same NAT) + // or peers and server inside LAN, means both peers have private IPs on public ip. + + if ((sesFrom.ClientPublicIp.Address.Equals(sesTo.ClientPublicIp.Address)) || + (IPHelper.IsPrivateIPAddress(sesFrom.ClientPublicIp) && IPHelper.IsPrivateIPAddress(sesTo.ClientPublicIp))) + { + foreach (var local in Locals_From_NeedsToKnow) + { + EndpointData data = new EndpointData(local, toPort); + FromNeedsToKnow.LocalEndpoints.Add(data); + } + + foreach (var local in Locals_To_NeedsToKnow) + { + EndpointData data = new EndpointData(local, fromPort); + ToNeedsToKnow.LocalEndpoints.Add(data); + } + } + + + // "From" is same network as the server. + if (IPHelper.IsPrivateIPAddress(sesFrom.ClientPublicIp)) + { + // "To" needs to get server adress to connect + // 0.0.0.0:0 means serverIp + ToNeedsToKnow.IpRemote = new byte[4]; + ToNeedsToKnow.PortRemote = fromPort; + + } + else + { + // send just the publicIp of "From" to "To" + ToNeedsToKnow.IpRemote = sesFrom.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + ToNeedsToKnow.PortRemote = fromPort; + } + + // "To" is same network as the server. + if (IPHelper.IsPrivateIPAddress(sesTo.ClientPublicIp)) + { + //"From" needs to get Server adress + FromNeedsToKnow.IpRemote = new byte[4]; + FromNeedsToKnow.PortRemote = toPort; + } + else + { + // send just the public to "From" + FromNeedsToKnow.IpRemote = sesTo.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + FromNeedsToKnow.PortRemote = toPort; + } + + } + + public static void ObtainIpEndpoints(EndpointTransferMessage fromAdresses, EndpointTransferMessage toAdresses, + out EndpointTransferMessage FromNeedsToKnow, out EndpointTransferMessage ToNeedsToKnow) + { + FromNeedsToKnow = new EndpointTransferMessage(); + ToNeedsToKnow = new EndpointTransferMessage(); + + var fromLocalIps = GetAdressesAsStrings(fromAdresses.LocalEndpoints); + var toLocalIps = GetAdressesAsStrings(toAdresses.LocalEndpoints); + int toPort = toAdresses.LocalEndpoints.First().Port; + int fromPort = fromAdresses.LocalEndpoints.First().Port; + + + IPHelper.ExtractLocalIpsWithMatchingSubnet(fromLocalIps, + toLocalIps, + out List Locals_From_NeedsToKnow, + out List Locals_To_NeedsToKnow); + + + //They need to know locals when both peers have public IPs same(coming from same NAT) + // or peers and server inside LAN, means both peers have private IPs on public ip. + + if (AreEqual(fromAdresses.IpRemote, toAdresses.IpRemote) || + (IPHelper.IsPrivateIPAddress(fromAdresses.IpRemote) && IPHelper.IsPrivateIPAddress(toAdresses.IpRemote))) + { + foreach (var local in Locals_From_NeedsToKnow) + { + EndpointData data = new EndpointData(local, toPort); + FromNeedsToKnow.LocalEndpoints.Add(data); + } + + foreach (var local in Locals_To_NeedsToKnow) + { + EndpointData data = new EndpointData(local, fromPort); + ToNeedsToKnow.LocalEndpoints.Add(data); + } + } + + + // "From" is same network as the server. + if (IPHelper.IsPrivateIPAddress(fromAdresses.IpRemote)) + { + // "To" needs to get server adress to connect + // 0.0.0.0:0 means serverIp + ToNeedsToKnow.IpRemote = new byte[4]; + ToNeedsToKnow.PortRemote = fromPort; + + } + else + { + // send just the publicIp of "From" to "To" + ToNeedsToKnow.IpRemote = fromAdresses.IpRemote; + ToNeedsToKnow.PortRemote = fromPort; + } + + // "To" is same network as the server. + if (IPHelper.IsPrivateIPAddress(toAdresses.IpRemote)) + { + //"From" needs to get Server adress + FromNeedsToKnow.IpRemote = new byte[4]; + FromNeedsToKnow.PortRemote = toPort; + } + else + { + // send just the public to "From" + FromNeedsToKnow.IpRemote = toAdresses.IpRemote; + FromNeedsToKnow.PortRemote = toPort; + } + + } + + private static List GetAdressesAsStrings(List localEndpoints) + { + List result = new List(); + foreach (var local in localEndpoints) + { + result.Add(Byte2Sting(local.Ip)); + } + return result; + } + + public static string Byte2Sting(byte[] ip) + { + return $"{ip[0]}.{ip[1]}.{ip[2]}.{ip[3]}"; + } + + public static byte[] String2Byte(string ip) + { + + var parts = ip.Split('.'); + + return new byte[] { + byte.Parse(parts[0]), + byte.Parse(parts[1]), + byte.Parse(parts[2]), + byte.Parse(parts[3]) + }; + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/ITimeProvider.cs b/NetworkLibrary/DistributedP2P/Components/ITimeProvider.cs new file mode 100644 index 0000000..d341002 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/ITimeProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Components +{ + public interface ITimeProvider + { + DateTime GetDateTime(); + double GetTime(); + + Task SyncTime(); + } +} \ No newline at end of file diff --git a/NetworkLibrary/DistributedP2P/Components/InternalConstants.cs b/NetworkLibrary/DistributedP2P/Components/InternalConstants.cs new file mode 100644 index 0000000..2b115dd --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/InternalConstants.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class InternalConstants + { + public const string ConnectionStart = "0"; + public const string ConnectionReq = "1"; + public const string ConnectionAckGood = "2"; + public const string ConnectionAckBad = "3"; + public const string Error = "4"; + public const string ConnectionGetClientPublicData = "5"; + public const string ConnectionAckClientPublicData = "6"; + public const string PipeRequestTcp = "7"; + public const string PipeRequestUdp = "8"; + public const string PipeReqAck = "d"; + public const string PipeTokenDeliveryTcp = "9"; + public const string PipeTokenDeliveryUdp = "a"; + public const string PublishPeerList = "b"; + public const string SyncTime = "c"; + public const string RequestHolepunchUdp = "d"; + public const string AckRequestHolepunchUdp = "e"; + public const string StartHP = "f"; + public const string PunchSucces = "g"; + public const string PunchFail = "h"; + public const string PunchSuccesAck = "i"; + public const string PunchFailAck = "j"; + public const string RequestSequentialHolepunchTcp = "k"; + public const string AckRequestHolepunchTcp = "l"; + public const string PunchSwap = "m"; + public const string RequestSimultaneousHolepunchTcp = "n"; + + public const string RequestCreateOrJointRoom = "o"; + public const string ResponseCreateOrJointRoom = "p"; + public const string JoinedRoom = "r"; + public const string KeepAlive = "s"; + + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/Logger.cs b/NetworkLibrary/DistributedP2P/Components/Logger.cs new file mode 100644 index 0000000..6a80da5 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/Logger.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace NetworkLibrary.DistributedP2P.Components +{ + [Flags] + public enum LogType + { + Debug = 1, + Info = 2, + Warning = 4, + Error = 8, + Exception = 16 + } + + // readonly structs does not make defensive copies. + public readonly struct LogData + { + public readonly long Id; + public readonly LogType LogType; + public readonly DateTime TimeStamp; + public readonly string Log; + + public LogData(long id, LogType logType, DateTime timeStamp, string log) + { + Id = id; + LogType = logType; + TimeStamp = timeStamp; + Log = log; + } + } + + + public class Logger : ILogger + { + public event Action LogAvailable; + + private LogType allowedLogTypes = LogType.Debug | LogType.Error | LogType.Warning | LogType.Info | LogType.Exception; + private Dictionary logTypeLookup; + private long logId = 0; + + public void Log(LogType logType, string log) + { + if (!(allowedLogTypes.HasFlag(logType))) + { + return; + } + + + var id = Interlocked.Increment(ref logId); + + LogData data = new LogData(id: id, + timeStamp: DateTime.UtcNow, + logType: logType, + log: log); + + try + { + LogAvailable?.Invoke(data); + } + catch (Exception e) + { + Trace.WriteLine($"#[Critical Error] Logger failed! : \n{e.ToString()}"); + } + + } + + public void Log(Exception ex) + { + if (!(allowedLogTypes.HasFlag(LogType.Exception))) + { + return; + } + + + var id = Interlocked.Increment(ref logId); + + LogData data = new LogData(id: id, + timeStamp: DateTime.UtcNow, + logType: LogType.Exception, + log: $"{ex.Message}\n{ex.StackTrace}"); + + try + { + LogAvailable?.Invoke(data); + } + catch (Exception e) + { + Trace.WriteLine($"#[Critical Error] Logger failed! : \n{e.ToString()}"); + } + + } + + public string Stringify(LogData data) + { + return $"#[{data.Id}][{data.TimeStamp}][{logTypeLookup[data.LogType]}] : {data.Log}"; + } + + + /// + /// allowedLogTypes = LogType.Debug | LogType.Error | LogType.Warning | LogType.Info | LogType.Exception + /// + /// + public void SetAllowedOptions(LogType allowedLogTypes) + { + this.allowedLogTypes = allowedLogTypes; + } + + // Constructed only once if anyone acesses this class(Thread safe). + public Logger() + { + InitializeLookUpTable(); + + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionExit; + AppDomain.CurrentDomain.ProcessExit += OnExit; + } + + private void InitializeLookUpTable() + { + logTypeLookup = new Dictionary() + { + {LogType.Info,"Info" }, + {LogType.Debug,"Debug" }, + {LogType.Warning,"Warning" }, + {LogType.Error,"Error" }, + {LogType.Exception,"Exception" }, + }; + } + + private void OnExit(object sender, EventArgs e) + { + Log(LogType.Info, "Application Exiting.."); + } + + private void UnhandledExceptionExit(object sender, UnhandledExceptionEventArgs e) + { + Log(LogType.Exception, $"Application Exit with an unhandled exception:" + + $"\n{((Exception)e.ExceptionObject).Message}" + + $"\nStack Trace:{((Exception)e.ExceptionObject).StackTrace}"); + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/NTPClient.cs b/NetworkLibrary/DistributedP2P/Components/NTPClient.cs new file mode 100644 index 0000000..57ddc53 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/NTPClient.cs @@ -0,0 +1,106 @@ +using NetworkLibrary.UDP; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Components +{ + public class TimeResult + { + public double PreciseTime { get; set; } + public TimeSpan DateTimeOffset { get; set; } + public bool Succes { get; set; } + public double RTT { get; set; } + public double Delay { get; set; } // Store calculated delay + public double AsymmetryEstimate { get; set; } // Store estimated path asymmetry + } + + + internal class NTPClient:IDisposable + { + + struct TimeData + { + public double ServersTime; + public double ArrivalTime; + } + + + ConcurrentDictionary> pending = new ConcurrentDictionary>(); + byte ctr = 0; + private AsyncUdpClient udpClient; + private Stopwatch clientClock; + + public NTPClient(string ip, int port, Stopwatch clientClock) + { + udpClient = new AsyncUdpClient(); + udpClient.OnBytesRecieved += HandleReceived; + udpClient.Connect(ip, port); + + this.clientClock = clientClock; + } + + private void HandleReceived(byte[] bytes, int offset, int count) + { + var arrivalTime = clientClock.Elapsed.TotalMilliseconds; + byte num = bytes[offset++]; + if (pending.TryRemove(num, out var tcs)) + { + double time = PrimitiveEncoder.ReadFixedDouble(bytes, offset); + tcs.SetResult(new TimeData() { ServersTime = time, ArrivalTime = arrivalTime }); + } + } + + public async Task GetServerTime(int delayMs) + { + var buff = new byte[1]; + var pollNum = ctr++; + buff[0] = pollNum; + var tcs = new TaskCompletionSource(); + pending[pollNum] = tcs; + + var t1 = clientClock.Elapsed.TotalMilliseconds; + udpClient.SendAsync(buff); + + var delayTask = Task.Delay(delayMs); + if (await Task.WhenAny(delayTask, tcs.Task).ConfigureAwait(false)== tcs.Task) + { + + var serverTime = tcs.Task.Result; + var t2 = serverTime.ServersTime; + var t3 = t2 + 0.005; + var t4 = serverTime.ArrivalTime; + + + var delay = (t4 - t1) - (t3 - t2); + var offset = ((t2 - t1) + (t3 - t4)) / 2; + + return new TimeResult() + { + PreciseTime = offset, + Succes = true, + RTT = t4 - t1, + Delay = delay, + }; + } + else + { + pending.TryRemove(pollNum, out _); + return new TimeResult() + { + Succes = false + }; + } + + + + + } + + public void Dispose() + { + udpClient?.Dispose(); + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/NTPServer.cs b/NetworkLibrary/DistributedP2P/Components/NTPServer.cs new file mode 100644 index 0000000..46c4312 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/NTPServer.cs @@ -0,0 +1,132 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class NTPServer : IDisposable + { + Socket udpListener; + Stopwatch sw; + + [ThreadStatic] + static byte[] buff = new byte[9]; + + int IsDisposed = 0; + + public NTPServer(int port, Stopwatch clock) + { + udpListener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + udpListener.Bind(new IPEndPoint(IPAddress.Any, port)); + sw = clock; + } + + + + internal void Start() + { + for (int i = 0; i < Environment.ProcessorCount; i++) + { + Receive(GetReceiveArgs()); + } + } + + private void Receive(SocketAsyncEventArgs sea) + { + if (!udpListener.ReceiveFromAsync(sea)) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => Received(null, sea), null); + } + } + + private void Received(object _, SocketAsyncEventArgs e) + { + while (true) + { + try + { + + if (e.SocketError != SocketError.Success) + { + e.Dispose(); + Receive(GetReceiveArgs()); + return; + } + + var time = sw.Elapsed.TotalMilliseconds; + var buff = GetBuffer(); + buff[0] = e.Buffer[0]; + PrimitiveEncoder.WriteFixedDouble(buff, 1, time); + + udpListener.SendTo(buff, 0, buff.Length, SocketFlags.None, e.RemoteEndPoint); + + var ep = (IPEndPoint)e.RemoteEndPoint; + ep.Address = IPAddress.Any; + ep.Port = 0; + + if (udpListener.ReceiveFromAsync(e)) + { + return; + } + } + catch (Exception ex) + { + if (Interlocked.CompareExchange(ref IsDisposed, 0, 0) == 0) + { + Log(ex.Message + "\n" + ex.StackTrace); + e.Dispose(); + Receive(GetReceiveArgs()); + return; + } + else + { + e.Dispose(); + return; + } + } + + } + + + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte[] GetBuffer() + { + if (buff == null) + { + buff = new byte[9]; + } + return buff; + } + + private SocketAsyncEventArgs GetReceiveArgs() + { + + var recArgs = new SocketAsyncEventArgs(); + recArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + recArgs.Completed += Received; + recArgs.SetBuffer(new byte[65555], 0, 65555); + + + return recArgs; + } + + private void Log(string v) + { + Console.WriteLine(v); + } + + public void Dispose() + { + if (Interlocked.CompareExchange(ref IsDisposed, 1, 0) == 0) + { + udpListener.Dispose(); + } + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/PreciseTimeAwaiter.cs b/NetworkLibrary/DistributedP2P/Components/PreciseTimeAwaiter.cs new file mode 100644 index 0000000..c5f838f --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/PreciseTimeAwaiter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class PreciseTimeAwaiter + { + static Stopwatch sw = Stopwatch.StartNew(); + public static void Wait(double miliseconds) + { + if (miliseconds <= 0) + return; + double time = sw.Elapsed.TotalMilliseconds; + double until = time+miliseconds; + + while (until > sw.Elapsed.TotalMilliseconds) + { + if((until - sw.Elapsed.TotalMilliseconds)>32) + Thread.Sleep(10);//~16 ms + else + Thread.SpinWait(20); + } + + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/StateManager.cs b/NetworkLibrary/DistributedP2P/Components/StateManager.cs new file mode 100644 index 0000000..da3b0bd --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/StateManager.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Concurrent; +using static NetworkLibrary.P2P.Components.PingData; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class StateManager + { + ConcurrentDictionary states = new ConcurrentDictionary(); + private readonly ILogger logger; + + public StateManager(ILogger logger) + { + this.logger = logger; + } + + public void RegisterState(IConversationState state) + { + state.OnComplete += HandleComplete; + states.TryAdd(state.StateId, state); + + } + + private void HandleComplete(IConversationState state) + { + UnregisterState(state.StateId); + state.OnComplete -= HandleComplete; + } + + public void UnregisterState(Guid stateId) + { + states.TryRemove(stateId, out _); + } + + public bool HandleMessage(MessageEnvelope message) + { + Guid stateId = message.MessageId; + + if (stateId == Guid.Empty) + { + return false; + } + + if (states.TryGetValue(stateId, out var state)) + { + try + { + state.HandleMessage(message); + } + catch (Exception e) + { + logger?.Log(LogType.Exception,$"State Management failed : {e.Message}\n{e.StackTrace}"); + state.Cancel(); + UnregisterState(stateId); + } + return true; + } + + return false; + } + + + } +} diff --git a/NetworkLibrary/DistributedP2P/Components/TimeSync.cs b/NetworkLibrary/DistributedP2P/Components/TimeSync.cs new file mode 100644 index 0000000..6e596f3 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/TimeSync.cs @@ -0,0 +1,456 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NetworkLibrary.Utils; +using NetworkLibrary.P2P; +using static NetworkLibrary.DistributedP2P.Components.TimeSync; +using NetworkLibrary.P2P.Components.HolePunch; + +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class TimeSync:IDisposable + { + + private Stopwatch clientClock = Stopwatch.StartNew(); + private double timeOffset; + private TimeSpan timeOffsetd; + private NTPClient client; + private readonly SemaphoreSlim asyncLock = new SemaphoreSlim(1, 1); + + IDistributedConnection connection; + + TimeSyncer timeSync = new TimeSyncer(); + bool cancel = false; + int maxPeriod = 30000; + Queue offsetHistory = new Queue(); + public TimeSync(IDistributedConnection connection) + { + this.connection = connection; + + } + + public void SetEndpoint(EndpointData endpointData) + { + string ip = IPHelper.Byte2Sting(endpointData.Ip); + var cl = new NTPClient(ip, endpointData.Port, clientClock); + Interlocked.Exchange(ref client, cl)?.Dispose(); + } + + public async void StartAutoTimeSync() + { + + int failureCount = 0; + cancel=false; + int PeriodLocal = 2000; + while (!cancel) + { + try + { + await Task.Delay(PeriodLocal).ConfigureAwait(false); + if (cancel) + return; + + bool result = await SyncTime().ConfigureAwait(false); + if (result == false) + { + if (++failureCount > 2) + { + StopAutoTimeSync(); + } + + } + else + { + failureCount = 0; + } + + PeriodLocal = Math.Min(PeriodLocal * 2, maxPeriod); + + } + catch (Exception e) + { + StopAutoTimeSync(); + return; + } + } + } + + public void StopAutoTimeSync() + { + cancel = true; + ClearData(); + } + + private async void ClearData() + { + try + { + await asyncLock.WaitAsync().ConfigureAwait(false); + offsetHistory.Clear(); + } + catch { } + finally + { + asyncLock.Release(); + } + + } + + public async Task SyncTime() + { + try + { + await asyncLock.WaitAsync().ConfigureAwait(false); + + + int sampleSize = 12; + + var localHistory = new List(); + while(localHistory.Count = 4) + { + var result= Statistics.FilterOutliers(offsetHistory); + timeOffset = result.Average(); + } + else + { + timeOffset = offsetHistory.Average(); + } + + if (offsetHistory.Count>5) + offsetHistory.Dequeue(); + + if(filteredSample.DateTimeOffset > timeOffsetd) + timeOffsetd = filteredSample.DateTimeOffset; + return true; + + } + finally + { + asyncLock.Release(); + } + + } + + private Task GetOffsetNTPUdp() + { + return client.GetServerTime(2000); + } + + + + + + private async Task GetOffsetNTPTcp() + { + var msg = new MessageEnvelope() + { + Header = Constants.TimeSync, + IsInternal = true, + }; + + var t1 = clientClock.Elapsed.TotalMilliseconds; + var t1d = DateTime.UtcNow; + + var response = await connection.SendMessageAndWaitResponse(msg).ConfigureAwait(false); + + var t4 = clientClock.Elapsed.TotalMilliseconds; + var t4d = DateTime.UtcNow; + + if (response.Header != MessageEnvelope.RequestTimeout) + { + var t2 = PrimitiveEncoder.ReadFixedDouble(response.Payload, response.PayloadOffset); + var t3 = t2 + 0.005; + + var t2d = response.TimeStamp; + var t3d = response.TimeStamp; + + // Calculate delay and offset using the full NTP formula + var delay = (t4 - t1) - (t3 - t2); + var offset = ((t2 - t1) + (t3 - t4)) / 2; + + var delayd = ((t4d - t1d) - (t3d - t2d)).TotalMilliseconds; + var offsetd = ((t2d - t1d) + (t3d - t4d)).TotalMilliseconds / 2; + + TimeSpan offd = TimeSpan.FromMilliseconds(offsetd); + + return new TimeResult() + { + PreciseTime = offset, + DateTimeOffset = offd, + Succes = true, + RTT = t4 - t1, + Delay = delay, + }; + } + + return new TimeResult(); + } + + public double GetTime() + { + return clientClock.Elapsed.TotalMilliseconds + timeOffset; + } + + public DateTime GetDateTime() + { + return DateTime.UtcNow.Add(timeOffsetd); + } + + public void Dispose() + { + client.Dispose(); + } + } + + internal class TimeSyncer + { + public List timeHistory = new List(); + + private TimeResult GetMinRtt(List timeHistory) + { + double min = int.MaxValue; + TimeResult result = null; + foreach (var item in timeHistory) + { + if (item.RTT < min) + { + result = item; + min = item.RTT; + } + } + return result; + } + + + public TimeResult GetBestTimeOffset(List samples, int sampleSize) + { + if (samples.Count < 4) return GetMinRtt(samples); + + //var lowRttSamples = samples.OrderBy(x => x.RTT).Take(sampleSize / 2); + + var lowRttSamples = GetEnhancedFilteredSamples(samples, sampleSize / 2); + + + // Calculate the median delay + var medianDelay = GetMedian(lowRttSamples.Select(s => s.Delay)); + + // Calculate asymmetry estimates for each sample + foreach (var sample in lowRttSamples) + { + // Positive values indicate forward path > return path + sample.AsymmetryEstimate = sample.Delay - medianDelay; + } + + // Filter outliers based on asymmetry estimates + var asymmetryValues = lowRttSamples.Select(s => s.AsymmetryEstimate); + var filteredAsymmetry = Statistics.FilterOutliers(asymmetryValues); + + // Only keep samples with acceptable asymmetry estimates + var finalSamples = lowRttSamples + .Where(s => filteredAsymmetry.Contains(s.AsymmetryEstimate)); + + + // Calculate weighted average based on RTT (lower RTT = higher weight) + double totalWeight = 0; + double weightedSumPrecise = 0; + double weightedSumDateTime = 0; + + foreach (var sample in finalSamples) + { + // Use inverse of RTT as weight + double weight = 1.0 / sample.RTT; + totalWeight += weight; + weightedSumPrecise += weight * sample.PreciseTime; + weightedSumDateTime += weight * sample.DateTimeOffset.TotalMilliseconds; + } + + return new TimeResult + { + PreciseTime = weightedSumPrecise / totalWeight, + DateTimeOffset = TimeSpan.FromMilliseconds(weightedSumDateTime / totalWeight), + Succes = true, + RTT = finalSamples.Average(s => s.RTT) + }; + } + + private double GetMedian(IEnumerable values) + { + var sortedValues = values.OrderBy(v => v).ToList(); + int count = sortedValues.Count; + + if (count % 2 == 0) + { + // Even count + return (sortedValues[count / 2 - 1] + sortedValues[count / 2]) / 2; + } + else + { + // Odd count + return sortedValues[count / 2]; + } + } + + + + public IEnumerable GetEnhancedFilteredSamples(List samples, int desiredCount = 6) + { + if (samples.Count <= desiredCount) return samples; + + // Phase 1: Initial RTT filtering to eliminate extreme outliers + var rttValues = samples.Select(s => s.RTT).ToList(); + var filteredRtt = Statistics.FilterOutliers(rttValues).ToList(); + var initialFiltered = samples.Where(s => filteredRtt.Contains(s.RTT)); + + if (initialFiltered.Count() <= desiredCount) return initialFiltered; + + // Phase 2: Calculate clustering of offset values + var clusters = ClusterSamples(initialFiltered); + + // Phase 3: Select best cluster based on size and consistency + var bestCluster = SelectBestCluster(clusters); + + // If best cluster has enough samples, use it + if (bestCluster.Count >= desiredCount) + { + return bestCluster.OrderBy(s => s.RTT).Take(desiredCount); + } + + // Otherwise, use hybrid approach combining RTT and consistency + return RankSamplesByQuality(initialFiltered).Take(desiredCount); + } + + // Cluster samples based on their offset values + private List> ClusterSamples(IEnumerable samples) + { + // Simple clustering algorithm based on offset proximity + const double clusterThreshold = 0.5; // ms - samples within 0.5ms of each other form a cluster + + var clusters = new List>(); + var remaining = new List(samples); + + while (remaining.Count > 0) + { + var currentSample = remaining[0]; + remaining.RemoveAt(0); + + var cluster = new List { currentSample }; + + // Find all samples close enough to this one to form a cluster + for (int i = remaining.Count - 1; i >= 0; i--) + { + if (Math.Abs(remaining[i].PreciseTime - currentSample.PreciseTime) <= clusterThreshold) + { + cluster.Add(remaining[i]); + remaining.RemoveAt(i); + } + } + + clusters.Add(cluster); + } + + return clusters; + } + + // Select the best cluster based on size and internal consistency + private List SelectBestCluster(List> clusters) + { + if (clusters.Count == 0) return new List(); + if (clusters.Count == 1) return clusters[0]; + + // Score each cluster + var scoredClusters = clusters.Select(cluster => new + { + Cluster = cluster, + // Size is important, but so is consistency of offset values + Size = cluster.Count, + Consistency = CalculateConsistency(cluster), + AvgRTT = cluster.Average(s => s.RTT) + }); + + // Prioritize larger clusters with better consistency and lower RTT + return scoredClusters + .OrderByDescending(c => c.Size * 5 + c.Consistency * 3 - c.AvgRTT) + .First() + .Cluster; + } + + // Calculate consistency score (lower is better) + private double CalculateConsistency(List cluster) + { + if (cluster.Count <= 1) return 0; + + var offsets = cluster.Select(s => s.PreciseTime); + double avg = offsets.Average(); + double variance = offsets.Sum(o => Math.Pow(o - avg, 2)) / cluster.Count; + + // Lower variance means higher consistency + return 1.0 / (1.0 + variance); + } + + // Rank samples by multiple quality indicators + private IEnumerable RankSamplesByQuality(IEnumerable samples) + { + int sampleCount = samples.Count(); + if (sampleCount <= 1) return samples; + + // Calculate average and standard deviation of all offset values + var offsets = samples.Select(s => s.PreciseTime); + double avgOffset = offsets.Average(); + double stdDev = Math.Sqrt(offsets.Sum(o => Math.Pow(o - avgOffset, 2)) / sampleCount); + + // Score each sample using multiple factors + var scoredSamples = samples.Select(sample => new + { + Sample = sample, + // How close to the average offset (lower is better) + OffsetDeviation = Math.Abs(sample.PreciseTime - avgOffset) / stdDev, + // RTT (lower is better) + NormalizedRTT = sample.RTT / samples.Average(s => s.RTT), + // If available, path asymmetry (lower is better) + AsymmetryScore = sample.AsymmetryEstimate != 0 ? + Math.Abs(sample.AsymmetryEstimate) / samples.Average(s => Math.Abs(s.AsymmetryEstimate)) : 1 + }); + + // Calculate composite score (lower is better) + var rankedSamples = scoredSamples + .Select(s => new + { + s.Sample, + Score = s.OffsetDeviation * 2 + s.NormalizedRTT + s.AsymmetryScore + }) + .OrderBy(s => s.Score) + .Select(s => s.Sample); + + + return rankedSamples; + } + + + + } + +} + + + + + + + diff --git a/NetworkLibrary/DistributedP2P/Components/TimerService.cs b/NetworkLibrary/DistributedP2P/Components/TimerService.cs new file mode 100644 index 0000000..80d1639 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Components/TimerService.cs @@ -0,0 +1,42 @@ +using NetworkLibrary.DistributedP2P.Server; +using System; +using System.Collections.Concurrent; +using System.Threading; +namespace NetworkLibrary.DistributedP2P.Components +{ + internal class TimerService + { + private static readonly ConcurrentDictionary timers = new ConcurrentDictionary(); + + public static void RegisterTimer(Guid timerId,int delay, Action OnTime) + { + var timer = new Timer(s => + { + try + { + OnTime?.Invoke(); + } + catch { } + + CancelTimeout(timerId); + + }, null, delay, Timeout.Infinite); + + timers.TryAdd(timerId, timer); + } + + public static void CancelTimeout(Guid guid) + { + if (timers.TryRemove(guid, out var timer)) + { + timer.Dispose(); + } + + } + + internal static void RegisterTimer(Guid stateId, object onTimeOut) + { + throw new NotImplementedException(); + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/DistributedLobbyServer.cs b/NetworkLibrary/DistributedP2P/Server/DistributedLobbyServer.cs new file mode 100644 index 0000000..4fef739 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/DistributedLobbyServer.cs @@ -0,0 +1,365 @@ +using NetworkLibrary.Components.Crypto.Certificate; +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.DistributedP2P.Server.StateManagement; +using NetworkLibrary.DistributedP2P.SimpleRelay; +using NetworkLibrary.MessageProtocol; +using NetworkLibrary.P2P; +using NetworkLibrary.P2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Server +{ + + public class Dependencies + { + public IAuthenticator Authenticator; + public IServerDbConnector DbConnector; + public ILogger logger; + } + public class ServerParameters + { + public X509Certificate2 certificate; + public int SSlPort; + public int TcpPort; + public int UdpPort; + public int DiscoveryServerPort; + } + public class DistributedLobbyServerBase : IServerConnection, IDisposable + { + public readonly int SSlPort; + public readonly int TcpRelayPort; + public readonly int UdpRelayPort; + public readonly int DiscoveryServerPort; + + private X509Certificate2 serverCertificate; + + SecureMessageServer sslServer; + + + IAuthenticator authenticator; + IServerDbConnector dbConnector; + ILogger logger; + + SessionManager sessionManager; + StateManager stateManager; + RelayService piper; + Stopwatch serverClock = new Stopwatch(); + + RelayService relayService; + RoomManager roomManager = new RoomManager(); + NTPServer ntpServer; + EndpointDiscoveryServer discoveryServer; + public DistributedLobbyServerBase(Dependencies dependencies, ServerParameters parameters) + { + authenticator = dependencies.Authenticator; + dbConnector = dependencies.DbConnector; + logger = dependencies.logger; + SSlPort = parameters.SSlPort; + TcpRelayPort = parameters.TcpPort; + UdpRelayPort = parameters.UdpPort; + DiscoveryServerPort = parameters.DiscoveryServerPort; + stateManager = new StateManager(logger); + serverCertificate = parameters.certificate ?? CertificateGenerator.GenerateSelfSignedCertificate(); + } + + + public void StartServer() + { + serverClock.Start(); + + sslServer = new SecureMessageServer(SSlPort, serverCertificate); + + ntpServer = new NTPServer(SSlPort, serverClock); + ntpServer.Start(); + + relayService = new RelayService(TcpRelayPort, UdpRelayPort); + + sslServer.OnClientRequestedConnection += ValidateSslConnection; + sslServer.OnClientAccepted += SslClientAccepted; + sslServer.OnClientDisconnected += SslClientDisconnected; + + sslServer.OnMessageReceived += SslMessageReceived; + sslServer.StartServer(); + + sessionManager = new SessionManager(this); + sessionManager.PeerListPublish += PublishPeerList; + + discoveryServer = new EndpointDiscoveryServer(DiscoveryServerPort); + discoveryServer.Start(); + } + + + + private bool ValidateSslConnection(Socket acceptedSocket) + { + // we will do Ddos protection here; + return true; + } + + + private void SslClientAccepted(Guid ephemeralClientId) + { + TimerService.RegisterTimer(ephemeralClientId, 20000, () => + { + if (!sessionManager.IsSessionActive(ephemeralClientId)) + { + sslServer.CloseSession(ephemeralClientId); + } + }); + } + + private void HandleConnRequest(MessageEnvelope msg) + { + + Guid stateId = msg.MessageId; + var state = new ServerConnectionState(stateId, msg.From, this, authenticator, dbConnector, DiscoveryServerPort, logger); + stateManager.RegisterState(state); + state.HandleMessage(msg); + + state.OnComplete += ConnectionStateComplete; + } + + private void ConnectionStateComplete(IConversationState state_) + { + var state = (ServerConnectionState)state_; + if (state.IsSuccesful) + { + var sessionEp = sslServer.GetSessionEndpoint(state.EphemeralClientId); + var statusList = sessionManager.CreateSession(state.clientDbInfo, state.EphemeralClientId, sessionEp, state.clientLocalIps); + //if (statusList != null) + // PublishPeerList(new List() { statusList }); + } + } + + private void PublishPeerList(List pubList) + { + var msg = new MessageEnvelope(); + msg.Header = InternalConstants.PublishPeerList; + msg.IsInternal = true; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + foreach (var pubData in pubList) + { + stream.Position32 = 0; + KnownTypeSerializer.SerializePeerStatusList(stream, pubData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + + sslServer.SendAsyncMessage(pubData.WhoNeedsToKnow, msg); + } + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + // so here message can be p2p message, clients business. + // internal messages + private void SslMessageReceived(Guid guid, MessageEnvelope envelope) + { + if (envelope.IsInternal) + { + HandleInternalMessage(guid, envelope); + } + else + { + sessionManager.RouteMessage(guid, envelope); + } + } + + private void HandleInternalMessage(Guid clientId, MessageEnvelope message) + { + + message.From = clientId; + + if (stateManager.HandleMessage(message)) + return; + + + + switch (message.Header) + { + case InternalConstants.ConnectionReq: + HandleConnRequest(message); + break; + + case InternalConstants.PipeRequestTcp: + + var pipeState = new ServerPipeState(message.MessageId, this, logger); + stateManager.RegisterState(pipeState); + pipeState.HandleMessage(message); + + break; + + + case InternalConstants.PipeRequestUdp: + + var pipeState1 = new ServerPipeState(message.MessageId, this, logger); + stateManager.RegisterState(pipeState1); + pipeState1.HandleMessage(message); + + break; + + case InternalConstants.RequestHolepunchUdp: + var state = new ServerUdpHolepunchState(message.MessageId, this, sessionManager, logger); + stateManager.RegisterState(state); + state.HandleMessage(message); + break; + + case InternalConstants.RequestSimultaneousHolepunchTcp: + var state2 = new ServerSimultaneousTcpHolepunchState(message.MessageId, this, sessionManager, logger); + stateManager.RegisterState(state2); + state2.HandleMessage(message); + break; + + case InternalConstants.RequestSequentialHolepunchTcp: + var state3 = new ServerSequentialTcpHolepunchState(message.MessageId, this, sessionManager, logger); + stateManager.RegisterState(state3); + state3.HandleMessage(message); + break; + + case Constants.TimeSync: + HandleTimeSync(clientId, message); + + break; + + case InternalConstants.KeepAlive: + sessionManager.HandleMessage(clientId,message); + break; + } + } + int ctr = 0; + Random r = new Random(42); + + private void HandleTimeSync(Guid clientId, MessageEnvelope message) + { + ////if (ctr++ % 2 == 0) + //{ + // await Task.Delay(r.Next(50, 300)); + //} + byte[] time = new byte[8]; + message.Payload = time; + PrimitiveEncoder.WriteFixedDouble(time, 0, serverClock.Elapsed.TotalMilliseconds); + message.TimeStamp = DateTime.UtcNow; + ////if (ctr % 3 == 0) + //{ + // await Task.Delay(r.Next(50, 300)); + //} + SendAsyncMessage(clientId, message); + } + + + void IServerConnection.GetPipeToken(bool isTcpPipe, Guid fromEphemeral, Guid toEphemeral, Action onReady) + { + // so we need to find a service here, on the relay services, prioritize locality then load. + var res = new PipeResult(); + res.IsSuccesfull = true; + res.Token = relayService.GetPipeToken(isTcpPipe); + res.PipeEndpoint = new EndpointData("0.0.0.0", isTcpPipe ? 20011 : 20012); + + onReady?.Invoke(res); + } + + + RoomResult IServerConnection.CreateOrJoinRoom(string roomName, string roomPassword, Guid peerId, RoomProtocol protocol) + { + var roomResult = roomManager.CreateOrJoinRoom(roomName, roomPassword,peerId, protocol); + + if (roomResult.IsCreated) + { + if (relayService.CreateRoom(roomResult.RoomInfo.RoomId, protocol)) + { + if(GetRoomToken(peerId, roomResult.RoomInfo.RoomId, out byte[] roomToken)) + { + roomResult.RoomToken = roomToken; + return roomResult; + } + return null; + } + + } + else + { + if (GetRoomToken(peerId, roomResult.RoomInfo.RoomId, out byte[] roomToken)) + { + roomResult.RoomToken = roomToken; + return roomResult; + } + } + + return null; + } + + private bool GetRoomToken(Guid peerId, Guid roomId, out byte[] token) + { + token = relayService.GetRoomToken(peerId, roomId); + return token != null; + } + + private bool RemoveFromRoom(Guid peerId, Guid roomId) + { + return true; + } + + public void SendAsyncMessage(Guid clientId, MessageEnvelope message) + { + sslServer.SendAsyncMessage(clientId, message); + } + + public void SendAsyncMessage(MessageEnvelope message) + { + sslServer.SendAsyncMessage(message.To, message); + } + public Task SendMessageAndWaitResponse(Guid destination, MessageEnvelope envelope) + { + return sslServer.SendMessageAndWaitResponse(destination, envelope); + } + + public Task SendMessageAndWaitResponse(MessageEnvelope envelope) + { + return sslServer.SendMessageAndWaitResponse(envelope.To, envelope); + } + + private void SslClientDisconnected(Guid ephemeralId) + { + sessionManager.DestroySession(ephemeralId); + } + public void EndSession (Guid ephemeralId) + { + sslServer.CloseSession(ephemeralId); + } + public void ShutDownServer() + { + sslServer.ShutdownServer(); + + } + + public DateTime GetDateTime() + { + // will be distributed time + return DateTime.UtcNow; + } + + public double GetTime() + { + return serverClock.Elapsed.TotalMilliseconds; + } + public Task SyncTime() + { + throw new NotImplementedException(); + } + public void Dispose() + { + sslServer.ShutdownServer(); + relayService.Dispose(); + discoveryServer.Dispose(); + ntpServer.Dispose(); + } + + + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/IAuthenticator.cs b/NetworkLibrary/DistributedP2P/Server/IAuthenticator.cs new file mode 100644 index 0000000..640a507 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/IAuthenticator.cs @@ -0,0 +1,20 @@ +using NetworkLibrary.MessageProtocol; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Server +{ + public interface IAuthenticationResult + { + bool IsValid { get; } + string UserId { get; } + string Error { get; } + IReadOnlyDictionary Claims { get; } + } + public interface IAuthenticator + { + Task Authenticate(string AuthenticationToken, string AuthenticationMethod, string Cookies); + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/IServerConnection.cs b/NetworkLibrary/DistributedP2P/Server/IServerConnection.cs new file mode 100644 index 0000000..d090fe3 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/IServerConnection.cs @@ -0,0 +1,18 @@ +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.DistributedP2P.Server.StateManagement; +using NetworkLibrary.P2P.Components.HolePunch; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using NetworkLibrary.DistributedP2P.SimpleRelay; + +namespace NetworkLibrary.DistributedP2P.Server +{ + internal interface IServerConnection : IDistributedConnection + { + void GetPipeToken(bool isTcpPipe, Guid fromEphemeral, Guid toEphemeral, Action onReady); + RoomResult CreateOrJoinRoom(string roomName, string roomPassword, Guid peerId, RoomProtocol protocol); + void EndSession(Guid ephemeralId); + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/IServerDbConnector.cs b/NetworkLibrary/DistributedP2P/Server/IServerDbConnector.cs new file mode 100644 index 0000000..d8cccca --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/IServerDbConnector.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Server +{ + public interface IClientDbInfo + { + bool IsValid { get; } + string Error { get; } + Guid ClientId { get; } + } + public interface IServerDbConnector + { + Task GetClientData(IAuthenticationResult result); + Task RegisterClient(IAuthenticationResult tokenResult, byte[] payload); + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/RoomManager.cs b/NetworkLibrary/DistributedP2P/Server/RoomManager.cs new file mode 100644 index 0000000..e9ca362 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/RoomManager.cs @@ -0,0 +1,88 @@ +using NetworkLibrary.DistributedP2P.SimpleRelay; +using System; +using System.Collections.Concurrent; +using System.Security.Cryptography; + +namespace NetworkLibrary.DistributedP2P.Server +{ + public class RoomInfo + { + public Guid RoomId { get; set; } + public string Name { get; set; } + + public byte[] RoomKey; + + public ConcurrentDictionary Peers = new ConcurrentDictionary(); + private ConcurrentQueue idStack = new ConcurrentQueue(); + + public RoomInfo() + { + for (byte i = 0; i < byte.MaxValue; i++) + { + idStack.Enqueue(i); + } + } + + internal bool AddPeer(Guid peerId, out byte idAlias) + { + idAlias = 0; + if (idStack.TryDequeue(out byte alias)) + { + if (Peers.TryAdd(peerId, alias)) + { + idAlias = alias; + return true; + } + else + { + idStack.Enqueue(alias); + return false; + } + } + return false; + + } + } + class RoomResult + { + public RoomInfo RoomInfo; + public bool IsCreated; + public byte idAlias; + public byte[] RoomToken; + } + internal class RoomManager + { + ConcurrentDictionary rooms = new ConcurrentDictionary(); + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + internal RoomResult CreateOrJoinRoom(string roomName, string roomPassword, Guid peerId, RoomProtocol protocol) + { + RoomResult result = new RoomResult(); + if (rooms.TryGetValue(roomName, out var roomInfo)) + { + if (roomInfo.AddPeer(peerId, out var Idalias)) + { + result.RoomInfo = roomInfo; + result.IsCreated = false; + result.idAlias = Idalias; + return result; + } + return null; + } + + var roomInf = new RoomInfo(); + roomInf.RoomId = Guid.NewGuid(); + roomInf.RoomKey = new byte[16]; + rng.GetBytes(roomInf.RoomKey); + + roomInf.AddPeer(peerId, out byte alias); + + rooms.TryAdd(roomName, roomInf); + result.RoomInfo = roomInfo; + result.IsCreated = false; + result.idAlias = alias; + + return result; + + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/ServerSession.cs b/NetworkLibrary/DistributedP2P/Server/ServerSession.cs new file mode 100644 index 0000000..7ec9958 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/ServerSession.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; + +namespace NetworkLibrary.DistributedP2P.Server +{ + + // should hold the data about the client. Keys etc everything. + class PeerStatusList + { + public Guid WhoNeedsToKnow; + public ConcurrentDictionary NewOnline = new ConcurrentDictionary(); + public ConcurrentDictionary WentOffline = new ConcurrentDictionary(); + + public bool IsEmpty() + { + return NewOnline.Count == 0 && WentOffline.Count == 0; + } + } + + public class PeerStatus + { + public Guid EphemeralId; + public Guid PeerId; + public DateTime OnlineSince; + } + internal class ServerSession + { + public IClientDbInfo ClientInfo { get; } + public Guid PeerId { get; internal set; } + public Guid EphemeralId { get; internal set; } + public IPEndPoint ClientPublicIp { get; } + public List ClientLocalIps { get; } + public DateTime OnlineSince { get; internal set; } + + public DateTime lastKeepAlive= DateTime.Now; + + private PeerStatusList statusList = new PeerStatusList(); + private ConcurrentDictionary onlinePeers = new ConcurrentDictionary(); + private PeerStatus status = new PeerStatus(); + + + public ServerSession(IClientDbInfo clientInfo, Guid ephemeralId, System.Net.IPEndPoint clientPublicIp, List clientLocalIps) + { + ClientInfo = clientInfo; + EphemeralId = ephemeralId; + ClientPublicIp = clientPublicIp; + ClientLocalIps = clientLocalIps; + PeerId = clientInfo.ClientId; + statusList.WhoNeedsToKnow = EphemeralId; + OnlineSince = DateTime.UtcNow; + + status.EphemeralId = EphemeralId; + status.PeerId = PeerId; + status.OnlineSince = OnlineSince; + + } + + internal bool Knows(Guid newPeer) + { + return true; + } + + internal void AddNewPeer(Guid ephemeralId, PeerStatus status) + { + if (onlinePeers.TryAdd(ephemeralId, status)) + { + statusList.NewOnline.TryAdd(ephemeralId, status); + statusList.WentOffline.TryRemove(ephemeralId, out _); + } + } + + internal void RemovePeer(Guid ephemeralId) + { + if (onlinePeers.TryRemove(ephemeralId, out var status)) + { + statusList.WentOffline.TryAdd(ephemeralId, status); + statusList.NewOnline.TryRemove(ephemeralId, out _); + } + } + + internal PeerStatus GetPeerStatus() + { + return status; + } + + internal PeerStatusList GetPublishInfo() + { + if (statusList.IsEmpty()) + return null; + // must be hard copy + // either hard copy or lock until all published over network. + PeerStatusList list = new PeerStatusList(); + list.WentOffline = new ConcurrentDictionary(statusList.WentOffline); + list.NewOnline = new ConcurrentDictionary(statusList.NewOnline); + list.WhoNeedsToKnow = EphemeralId; + + return list; + } + + internal void ResetPublishInfo() + { + statusList.WentOffline.Clear(); + statusList.NewOnline.Clear(); + } + + internal void KeepAliveMark() + { + lastKeepAlive = DateTime.Now; + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/SessionManager.cs b/NetworkLibrary/DistributedP2P/Server/SessionManager.cs new file mode 100644 index 0000000..9acb017 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/SessionManager.cs @@ -0,0 +1,166 @@ +using NetworkLibrary.DistributedP2P.Components; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Server +{ + + + + internal class SessionManager + { + internal ConcurrentDictionary serverSessions = new ConcurrentDictionary(); + + internal HashSet lastSnapshot = new HashSet(); + private TaskCompletionSource publishTrigger = new TaskCompletionSource(); + + public event Action> PeerListPublish; + + IServerConnection serverConnection; + private bool shutdown; + private object publishMutex = new object(); + public SessionManager(IServerConnection serverConnection) + { + this.serverConnection = serverConnection; + PublishRoutine(); + KeepAliveRoutine(); + } + + private async void KeepAliveRoutine() + { + try + { + while (true) + { + await Task.Delay(10000).ConfigureAwait(false); + var now = DateTime.Now; + foreach (var ses in serverSessions) + { + if ((now - ses.Value.lastKeepAlive).TotalMilliseconds > 10000) + { + serverConnection.EndSession(ses.Key); + DestroySession(ses.Key); + } + } + } + + } + catch { } + } + + internal bool RouteMessage(Guid from, MessageEnvelope envelope) + { + Guid to = envelope.To; + envelope.To = Guid.Empty; + envelope.From = from; + serverConnection.SendAsyncMessage(to, envelope); + + return true; + } + internal bool HandleMessage(Guid from, MessageEnvelope envelope) + { + switch (envelope.Header) + { + case InternalConstants.KeepAlive: + if (serverSessions.TryGetValue(from, out var session)) + { + session.KeepAliveMark(); + var msg = new MessageEnvelope(); + msg.Header = InternalConstants.KeepAlive; + msg.IsInternal = true; + serverConnection.SendAsyncMessage(from,msg); + } + break; + } + return true; + } + + public PeerStatusList CreateSession(IClientDbInfo clientInfo, Guid ephemeralClientId, IPEndPoint clientPublicIp, List clientLocalIps) + { + var newSession = new ServerSession(clientInfo, ephemeralClientId, clientPublicIp, clientLocalIps); + lock (publishMutex) + { + if (!serverSessions.ContainsKey(ephemeralClientId)) + { + foreach (var existingSessionKV in serverSessions) + { + if (existingSessionKV.Value.Knows(newSession.PeerId)) + { + existingSessionKV.Value.AddNewPeer(ephemeralClientId, newSession.GetPeerStatus()); + newSession.AddNewPeer(existingSessionKV.Value.EphemeralId, existingSessionKV.Value.GetPeerStatus()); + } + } + serverSessions.TryAdd(ephemeralClientId, newSession); + + } + // for instant sync of new peer + var pubInfo = newSession.GetPublishInfo(); + //newSession.ResetPublishInfo(); + + publishTrigger.TrySetResult(true); + return pubInfo; + } + + } + + internal void DestroySession(Guid ephemeralClientId) + { + lock (publishMutex) + { + if (serverSessions.TryRemove(ephemeralClientId, out ServerSession session)) + { + foreach (var existingSessionKV in serverSessions) + { + existingSessionKV.Value.RemovePeer(ephemeralClientId); + } + publishTrigger.TrySetResult(true); + } + } + } + + internal bool IsSessionActive(Guid ephemeralClientId) + { + return serverSessions.ContainsKey(ephemeralClientId); + } + + + internal bool GetSessionData(Guid guid, out ServerSession sesData) + { + return serverSessions.TryGetValue(guid, out sesData); + + } + + internal async void PublishRoutine() + { + List PubList = new List(); + while (!shutdown) + { + await publishTrigger.Task; + Interlocked.Exchange(ref publishTrigger, new TaskCompletionSource()); + + PubList.Clear(); + + lock (publishMutex) + { + foreach (var sessionKV in serverSessions) + { + PeerStatusList toPublish = sessionKV.Value.GetPublishInfo(); + if (toPublish != null) + { + sessionKV.Value.ResetPublishInfo(); + PubList.Add(toPublish); + } + } + } + + + PeerListPublish?.Invoke(PubList); + await Task.Delay(1000); + } + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerConnectionState.cs b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerConnectionState.cs new file mode 100644 index 0000000..55f87a0 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerConnectionState.cs @@ -0,0 +1,205 @@ +using NetworkLibrary.DistributedP2P.Components; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Server.StateManagement +{ + internal class ServerConnectionState : ConversationStateBase + { + public IClientDbInfo clientDbInfo { get; private set; } + + public readonly Guid EphemeralClientId; + + private readonly IDistributedConnection connection; + private readonly IAuthenticator authenticator; + private readonly IServerDbConnector dbConnector; + private readonly int endpointDiscoveryServerPort; + private IAuthenticationResult tokenResult; + + public List clientLocalIps; + + int timeSynced; + int handShakeComplete; + + public ServerConnectionState(Guid stateId, Guid clientId, IDistributedConnection connection, IAuthenticator authenticator, IServerDbConnector dbConnector, int EDSPort, ILogger logger) : base(stateId, 20000, logger) + { + this.EphemeralClientId = clientId; + this.connection = connection; + this.authenticator = authenticator; + this.dbConnector = dbConnector; + endpointDiscoveryServerPort = EDSPort; + } + + + public override void HandleMessage(MessageEnvelope message) + { + if (IsCompleted()) + return; + switch (message.Header) + { + case InternalConstants.ConnectionReq: + HandleInitialConnectionRequest(message); + break; + + case InternalConstants.SyncTime: + HandleTimeSyncComplete(message); + break; + + case InternalConstants.ConnectionAckClientPublicData: + HandleClientPublicData(message); + break; + } + } + + + + private void HandleInitialConnectionRequest(MessageEnvelope message) + { + message.KeyValuePairs.TryGetValue("AuthToken", out string token); + message.KeyValuePairs.TryGetValue("AuthMethod", out string method); + message.KeyValuePairs.TryGetValue("AdditionalData", out string additionalData); + + if (token == null) + { + ReplyError("No token provided"); + return; + } + + if (method == null) + { + ReplyError("No authentication method provided"); + return; + } + message.KeyValuePairs.Remove("AuthToken"); + message.KeyValuePairs.Remove("AuthMethod"); + if (message.KeyValuePairs.ContainsKey("AdditionalData")) + message.KeyValuePairs.Remove("AdditionalData"); + + clientLocalIps = new List(); + + foreach (var kv in message.KeyValuePairs) + { + clientLocalIps.Add(kv.Key); + } + + authenticator.Authenticate(token, method, additionalData).ContinueWith(HandleAuthentication, TaskScheduler.Default); + } + + private void HandleAuthentication(Task task) + { + if (IsCompleted()) + return; + + IAuthenticationResult result = task.Result; + + if (result.IsValid) + { + FindClientDBLink(result); + } + else + { + ReplyError(result.Error); + } + } + + private void FindClientDBLink(IAuthenticationResult tokenResult) + { + dbConnector.GetClientData(tokenResult).ContinueWith(t => HandleClientData(t.Result, tokenResult), TaskScheduler.Default); + } + + private void HandleClientData(IClientDbInfo dbResult, IAuthenticationResult tokenResult) + { + if (IsCompleted()) + return; + + if (dbResult.IsValid) + { + clientDbInfo = dbResult; + HandShakecomplete(); + } + else + { + this.tokenResult = tokenResult; + RegisterClient(); + } + } + + + + private void RegisterClient() + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.ConnectionGetClientPublicData; + connection.SendAsyncMessage(EphemeralClientId, msg); + } + + private void HandleClientPublicData(MessageEnvelope message) + { + message.LockBytes(); + dbConnector.RegisterClient(tokenResult, message.Payload).ContinueWith(HandleDbRegistration, TaskScheduler.Default); + } + + private void HandleDbRegistration(Task task) + { + if (IsCompleted()) + return; + + IClientDbInfo dbResult = task.Result; + if (dbResult.IsValid) + { + clientDbInfo = dbResult; + HandShakecomplete(); + } + else + { + ReplyError(dbResult.Error); + } + } + + private void HandleTimeSyncComplete(MessageEnvelope message) + { + if (Interlocked.Exchange(ref timeSynced, 1) == 0) + { + if (Interlocked.CompareExchange(ref handShakeComplete, 0, 0) == 1) + SendGood(); + } + + } + + private void HandShakecomplete() + { + if (Interlocked.Exchange(ref handShakeComplete, 1) == 0) + { + if (Interlocked.CompareExchange(ref timeSynced, 0, 0) == 1) + SendGood(); + } + } + + private void SendGood() + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.ConnectionAckGood; + msg.To = EphemeralClientId; + msg.KeyValuePairs = new Dictionary(); + msg.KeyValuePairs["EDPort"] = endpointDiscoveryServerPort.ToString(); + lock (cancellationMutex) + { + if (IsCompleted()) + return; + + connection.SendAsyncMessage(EphemeralClientId, msg); + Completed(true); + } + } + + private void ReplyError(string err) + { + connection.SendAsyncMessage(EphemeralClientId, CreateErrorMsg(err)); + Completed(false); + } + + + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerPipeState.cs b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerPipeState.cs new file mode 100644 index 0000000..08675f7 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerPipeState.cs @@ -0,0 +1,193 @@ +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.DistributedP2P.Server.StateManagement +{ + class PipeData + { + public const int TokenLength = 32 + 24;//32 bytes signature, 24 bytes token + public byte[] Token { get; set; } + public byte[] DHPublic { get; set; } + // localhost, localip, publicip + public EndpointData PipeEndpoint { get; set; } + } + class PipeResult + { + public byte[] Token { get; set; } + public EndpointData PipeEndpoint { get; set; } + public bool IsSuccesfull { get; internal set; } + } + + internal class ServerPipeState : ConversationStateBase + { + private Guid from, to; + private int ackCount = 0; + private readonly IServerConnection connection; + + private string ChannelType; + bool isTcpPipe = false; + private byte[] destinationsDhPublicKey; + + private ChannelInfo chInfo = new ChannelInfo(); + public ServerPipeState(Guid stateId, IServerConnection connection, ILogger logger) : base(stateId, 20000, logger) + { + this.connection = connection; + } + + /* + * C1 wants pope req with C2 + * Server finds a suitible Relay to pipe on + * Server obtains token from relay + * Server sends conn info and token to C1 and C2 + * + * C2 then needs to verify C1 optionally, with this you know that token came from trusted server and client is who he says he is. + * + */ + + public override void HandleMessage(MessageEnvelope message) + { + try + { + switch (message.Header) + { + case InternalConstants.PipeRequestTcp: + HandlePipeRequest(message, true); + break; + case InternalConstants.PipeRequestUdp: + HandlePipeRequest(message, false); + break; + + case InternalConstants.PipeReqAck://do nack also + HandlePipeReqAck(message); + break; + + case InternalConstants.ConnectionAckGood: + HandleGoodAck(message); + break; + + case InternalConstants.ConnectionAckBad: + HandleBadAck(); + break; + + } + + } + catch (Exception ex) + { + Log(LogType.Exception, $"Exception occured on server pipe state: {ex.Message}\n{ex.StackTrace}"); + HandleBadAck(); + } + + + } + //[A] + private void HandlePipeRequest(MessageEnvelope message, bool tcp) + { + this.from = message.From; + this.to = message.To; + + int offs = message.PayloadOffset; + var clientPipeData = KnownTypeSerializer.DeserializeClientPipeData(message.Payload, ref offs); + + chInfo = clientPipeData.ChannelInfo; + + connection.SendAsyncMessage(message);// dest will know dh token in this. + isTcpPipe = tcp; + } + //[B] + private void HandlePipeReqAck(MessageEnvelope message) + { + int offs = message.PayloadOffset; + var clientPipeData = KnownTypeSerializer.DeserializeClientPipeData(message.Payload, ref offs); + + if (chInfo.RequiresKeyExchange()) + destinationsDhPublicKey = clientPipeData.DHPublic;// requester will now this on pipetoken + + connection.GetPipeToken(isTcpPipe, from, to, HandlePipeResult); + + } + + private void HandlePipeResult(PipeResult result) + { + if (result.IsSuccesfull) + { + var data = new PipeData + { + Token = result.Token, + PipeEndpoint = result.PipeEndpoint + }; + HandlePipeToken(data); + } + else + { + Log(LogType.Error, "Unable to obtain pipe token"); + HandleBadAck(); + } + + } + + + private void HandlePipeToken(PipeData data) + { + var msg = CreateEnvelope(); + msg.Header = isTcpPipe ? InternalConstants.PipeTokenDeliveryTcp : InternalConstants.PipeTokenDeliveryUdp; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + // destination already knows the public key + KnownTypeSerializer.SerializePipeData(stream, data); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(to, msg); + + // requester will know from this message + data.DHPublic = destinationsDhPublicKey; + stream.Position32 = 0; + + KnownTypeSerializer.SerializePipeData(stream, data); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(from, msg); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + + private void HandleBadAck() + { + lock (cancellationMutex) + { + if (IsCompleted()) + return; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.ConnectionAckBad; + connection.SendAsyncMessage(from, msg); + connection.SendAsyncMessage(to, msg); + Completed(false); + } + } + + private void HandleGoodAck(MessageEnvelope message) + { + if (Interlocked.Increment(ref ackCount) == 2) + { + lock (cancellationMutex) + { + if (IsCompleted()) + return; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.ConnectionAckGood; + connection.SendAsyncMessage(from, msg); + connection.SendAsyncMessage(to, msg); + Completed(true); + } + } + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerRoomState.cs b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerRoomState.cs new file mode 100644 index 0000000..fe37db0 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerRoomState.cs @@ -0,0 +1,43 @@ +using NetworkLibrary.DistributedP2P.Components; +using System; + +namespace NetworkLibrary.DistributedP2P.Server.StateManagement +{ + internal class ServerRoomState : ConversationStateBase + { + private readonly IServerConnection connection; + + public ServerRoomState(Guid stateId, int timeout, IServerConnection connection, ILogger logger) : base(stateId, timeout, logger) + { + this.connection = connection; + } + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.RequestCreateOrJointRoom: + HandleRoomCreation(message); + break; + case InternalConstants.JoinedRoom: + HandleSuccess(message); + break; + } + } + + private async void HandleRoomCreation(MessageEnvelope message) + { + var roomName = message.KeyValuePairs["RoomName"]; + var password = message.KeyValuePairs["Pass"]; + + // room names are unique, end of story! + + //await connection.CreateOrJoinRoom(roomName, password); + } + + private void HandleSuccess(MessageEnvelope message) + { + throw new NotImplementedException(); + } + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerSequentialTcpHolepunchState.cs b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerSequentialTcpHolepunchState.cs new file mode 100644 index 0000000..e5df059 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerSequentialTcpHolepunchState.cs @@ -0,0 +1,182 @@ +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; + +namespace NetworkLibrary.DistributedP2P.Server.StateManagement +{ + internal class ServerSequentialTcpHolepunchState : ConversationStateBase + { + private readonly IDistributedConnection connection; + private readonly SessionManager sessionManager; + Guid From; + Guid To; + EndpointTransferMessage fromAdresses; + byte[] fromPublicKey; + + EndpointTransferMessage toAddresses; + byte[] toPublicKey; + + ChannelInfo info; + private int succesCount; + + public ServerSequentialTcpHolepunchState(Guid stateId, IDistributedConnection connection, SessionManager sessionManager, ILogger logger) : base(stateId, 20000, logger) + { + this.connection = connection; + this.sessionManager = sessionManager; + } + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.RequestSequentialHolepunchTcp: + HandleHolepunchRequest(message); + break; + case InternalConstants.AckRequestHolepunchTcp: + HandleHolepunchRequestAck(message); + break; + case InternalConstants.PunchSucces: + HandleSucces(message); + break; + + case InternalConstants.PunchSwap: + RelaySwapMsg(message); + break; + case InternalConstants.PunchFail: + HandleFailure(message); + break; + } + } + + private void RelaySwapMsg(MessageEnvelope message) + { + if (message.From == From) + { + connection.SendAsyncMessage(To, message); + } + else + { + connection.SendAsyncMessage(From, message); + } + } + + // obtain port from destination endpoint + private void HandleHolepunchRequest(MessageEnvelope message) + { + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + info = hpData.ChannelInfo; + fromPublicKey = hpData.DHPublic; + fromAdresses = hpData.Endpoints; + + From = message.From; + To = message.To; + + hpData.Endpoints = null; + hpData.DHPublic = null; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + message.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(message); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + + } + + private void HandleHolepunchRequestAck(MessageEnvelope message) + { + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + toPublicKey = hpData.DHPublic; + toAddresses = hpData.Endpoints; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.StartHP; + msg.KeyValuePairs = new Dictionary(); + + sessionManager.GetSessionData(From, out ServerSession sesFrom); + sessionManager.GetSessionData(To, out ServerSession sesTo); + + + + if (sesFrom != null && sesTo != null) + { + if (IPHelper.IsZero(toAddresses.IpRemote)) + toAddresses.IpRemote = sesTo.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + if (IPHelper.IsZero(fromAdresses.IpRemote)) + fromAdresses.IpRemote = sesFrom.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + + IPHelper.ObtainIpEndpoints(fromAdresses, + toAddresses, + out var FromNeedsToKnow, + out var ToNeedsToKnow); + + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + msg.To = From; + hpData.Endpoints = FromNeedsToKnow; + hpData.DHPublic = toPublicKey; + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + stream.Position32 = 0; + + msg.To = To; + hpData.Endpoints = ToNeedsToKnow; + hpData.DHPublic = fromPublicKey; + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + else + { + Cancel(); + } + + } + + protected override void Completed(bool succes) + { + Log(LogType.Debug, "Server Tcp Holepunch Finalized"); + base.Completed(succes); + } + + + private void HandleFailure(MessageEnvelope message) + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchFailAck; + connection.SendAsyncMessage(From, msg); + connection.SendAsyncMessage(To, msg); + Completed(false); + + } + int succCounter = 0; + + private void HandleSucces(MessageEnvelope message) + { + if (Interlocked.Increment(ref succCounter) == 2) + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSuccesAck; + connection.SendAsyncMessage(To, msg); + connection.SendAsyncMessage(From, msg); + Completed(true); + + } + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerSimultaneousTcpHolepunchState.cs b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerSimultaneousTcpHolepunchState.cs new file mode 100644 index 0000000..fa89c7a --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerSimultaneousTcpHolepunchState.cs @@ -0,0 +1,208 @@ +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; + +namespace NetworkLibrary.DistributedP2P.Server.StateManagement +{ + internal class ServerSimultaneousTcpHolepunchState : ConversationStateBase + { + private readonly IDistributedConnection connection; + private readonly SessionManager sessionManager; + Guid From; + Guid To; + EndpointTransferMessage fromAdresses; + byte[] fromPublicKey; + + EndpointTransferMessage toAddresses; + byte[] toPublicKey; + + ChannelInfo info; + private int succesCount; + + public ServerSimultaneousTcpHolepunchState(Guid stateId, IDistributedConnection connection, SessionManager sessionManager, ILogger logger) : base(stateId, 20000, logger) + { + this.connection = connection; + this.sessionManager = sessionManager; + } + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.RequestSimultaneousHolepunchTcp: + HandleHolepunchRequest(message); + break; + case InternalConstants.AckRequestHolepunchTcp: + HandleHolepunchRequestAck(message); + break; + case InternalConstants.PunchSucces: + HandleSucces(message); + break; + case InternalConstants.PunchFail: + HandleFailure(message); + break; + } + } + + // obtain port from destination endpoint + private void HandleHolepunchRequest(MessageEnvelope message) + { + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + info = hpData.ChannelInfo; + fromPublicKey = hpData.DHPublic; + fromAdresses = hpData.Endpoints; + + From = message.From; + To = message.To; + + hpData.Endpoints = null; + hpData.DHPublic = null; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + message.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(message); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + private void HandleHolepunchRequestAck(MessageEnvelope message) + { + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + toPublicKey = hpData.DHPublic; + toAddresses = hpData.Endpoints; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.StartHP; + msg.KeyValuePairs = new Dictionary(); + msg.KeyValuePairs["Time"] = ((connection.GetTime() + 500) + (500 * Math.Max(fromAdresses.LocalEndpoints.Count, toAddresses.LocalEndpoints.Count))).ToString(); + + + sessionManager.GetSessionData(From, out ServerSession sesFrom); + sessionManager.GetSessionData(To, out ServerSession sesTo); + + + + if (sesFrom != null && sesTo != null) + { + if (IPHelper.IsZero(toAddresses.IpRemote)) + toAddresses.IpRemote = sesTo.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + if (IPHelper.IsZero(fromAdresses.IpRemote)) + fromAdresses.IpRemote = sesFrom.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + + IPHelper.ObtainIpEndpoints(fromAdresses, + toAddresses, + out var FromNeedsToKnow, + out var ToNeedsToKnow); + + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + msg.To = From; + hpData.Endpoints = FromNeedsToKnow; + hpData.DHPublic = toPublicKey; + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + stream.Position32 = 0; + + msg.To = To; + hpData.Endpoints = ToNeedsToKnow; + hpData.DHPublic = fromPublicKey; + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + else + { + Cancel(); + } + + } + + + + + private void HandleFailure(MessageEnvelope message) + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchFailAck; + connection.SendAsyncMessage(From, msg); + connection.SendAsyncMessage(To, msg); + Completed(false); + + } + + private void HandleSucces(MessageEnvelope message) + { + if(Interlocked.Increment(ref succesCount) == 1) + { + var sts = message.KeyValuePairs["Status"]; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSuccesAck; + msg.KeyValuePairs = new Dictionary(); + msg.SetPayload(message.Payload, message.PayloadOffset, message.PayloadCount); + + if (sts == "Accepted") + { + + if (message.From == From) + { + msg.KeyValuePairs["Use"] = "Connected"; + connection.SendAsyncMessage(To, msg); + + msg.KeyValuePairs["Use"] = "Accepted"; + connection.SendAsyncMessage(From, msg); + } + else if (message.From == To) + { + msg.KeyValuePairs["Use"] = "Connected"; + connection.SendAsyncMessage(From, msg); + + msg.KeyValuePairs["Use"] = "Accepted"; + connection.SendAsyncMessage(To, msg); + } + + } + else // connected + { + + if (message.From == From) + { + msg.KeyValuePairs["Use"] = "Accepted"; + connection.SendAsyncMessage(To, msg); + + msg.KeyValuePairs["Use"] = "Connected"; + connection.SendAsyncMessage(From, msg); + } + else if (message.From == To) + { + msg.KeyValuePairs["Use"] = "Accepted"; + connection.SendAsyncMessage(From, msg); + + msg.KeyValuePairs["Use"] = "Connected"; + connection.SendAsyncMessage(To, msg); + } + } + Completed(true); + + } + + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerUdpHolepunchState.cs b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerUdpHolepunchState.cs new file mode 100644 index 0000000..611c813 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/Server/StateManagement/ServerUdpHolepunchState.cs @@ -0,0 +1,165 @@ +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.P2P.Components.HolePunch; +using NetworkLibrary.Utils; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; + +namespace NetworkLibrary.DistributedP2P.Server.StateManagement +{ + internal class ServerUdpHolepunchState : ConversationStateBase + { + private readonly IDistributedConnection connection; + private readonly SessionManager sessionManager; + + Guid From; + Guid To; + EndpointTransferMessage fromAdresses; + byte[] fromPublicKey; + + EndpointTransferMessage toAddresses; + byte[] toPublicKey; + + ChannelInfo info; + private int succesCount; + + public ServerUdpHolepunchState(Guid stateId, IDistributedConnection connection, SessionManager sessionManager, ILogger logger) : base(stateId, 20000, logger) + { + this.connection = connection; + this.sessionManager = sessionManager; + } + + public override void HandleMessage(MessageEnvelope message) + { + switch (message.Header) + { + case InternalConstants.RequestHolepunchUdp: + HandleHolepunchRequest(message); + break; + case InternalConstants.AckRequestHolepunchUdp: + HandleHolepunchRequestAck(message); + break; + case InternalConstants.PunchSucces: + HandleSucces(message); + break; + case InternalConstants.PunchFail: + HandleFailure(message); + break; + } + } + + // obtain port from destination endpoint + private void HandleHolepunchRequest(MessageEnvelope message) + { + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + + info = hpData.ChannelInfo; + fromPublicKey = hpData.DHPublic; + fromAdresses = hpData.Endpoints; + + From = message.From; + To = message.To; + + hpData.Endpoints = null; + hpData.DHPublic = null; + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + message.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(message); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + + private void HandleHolepunchRequestAck(MessageEnvelope message) + { + + int offs = message.PayloadOffset; + var hpData = KnownTypeSerializer.DeserializeHolepunchData(message.Payload, ref offs); + toPublicKey = hpData.DHPublic; + toAddresses = hpData.Endpoints; + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.StartHP; + msg.KeyValuePairs = new Dictionary(); + msg.KeyValuePairs["Time"] = ((connection.GetTime() + 500) + (200*Math.Max(fromAdresses.LocalEndpoints.Count, toAddresses.LocalEndpoints.Count))).ToString(); + + sessionManager.GetSessionData(From, out ServerSession sesFrom); + sessionManager.GetSessionData(To, out ServerSession sesTo); + + + + if (sesFrom != null && sesTo != null) + { + if (IPHelper.IsZero(toAddresses.IpRemote)) + toAddresses.IpRemote = sesTo.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + if (IPHelper.IsZero(fromAdresses.IpRemote)) + fromAdresses.IpRemote = sesFrom.ClientPublicIp.Address.MapToIPv4().GetAddressBytes(); + + IPHelper.ObtainIpEndpoints(fromAdresses, + toAddresses, + out var FromNeedsToKnow, + out var ToNeedsToKnow); + + + var stream = SharerdMemoryStreamPool.RentStreamStatic(); + + msg.To = From; + hpData.Endpoints = FromNeedsToKnow; + hpData.DHPublic = toPublicKey; + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + stream.Position32 = 0; + + msg.To = To; + hpData.Endpoints = ToNeedsToKnow; + hpData.DHPublic = fromPublicKey; + KnownTypeSerializer.SerializeHolepunchData(stream, hpData); + msg.SetPayload(stream.GetBuffer(), 0, stream.Position32); + connection.SendAsyncMessage(msg); + + SharerdMemoryStreamPool.ReturnStreamStatic(stream); + } + else + { + Cancel(); + } + + } + + + private void HandleFailure(MessageEnvelope message) + { + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchFailAck; + connection.SendAsyncMessage(From, msg); + connection.SendAsyncMessage(To, msg); + Completed(false); + + } + + private void HandleSucces(MessageEnvelope message) + { + + var msg = CreateEnvelope(); + msg.Header = InternalConstants.PunchSuccesAck; + msg.SetPayload(message.Payload, message.PayloadOffset, message.PayloadCount); + if (message.From == From) + connection.SendAsyncMessage(To, msg); + else if(message.From == To) + connection.SendAsyncMessage(From, msg); + + if (Interlocked.Increment(ref succesCount) == 2) + { + Completed(true); + } + } + + } +} diff --git a/NetworkLibrary/DistributedP2P/SimpleRelay/PeerRoomState.cs b/NetworkLibrary/DistributedP2P/SimpleRelay/PeerRoomState.cs new file mode 100644 index 0000000..05eadb5 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/SimpleRelay/PeerRoomState.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.SimpleRelay +{ + internal class PeerRoomState + { + public Guid RoomId { get; internal set; } + public Guid ExpectedClient { get; internal set; } + public Guid EphemeralId { get; internal set; } + public IPEndPoint AssociatedEndpoint { get; internal set; } + public bool isTcp; + + internal bool Verify(PipeToken token, Guid ephemeralId) + { + if(ExpectedClient.Equals(token.Token)) + { + isTcp = true; + EphemeralId = ephemeralId; + return true; + } + return false; + } + + internal bool Verify(PipeToken token, IPEndPoint clientEp) + { + if (ExpectedClient.Equals(token.Token)) + { + AssociatedEndpoint = clientEp; + return true; + } + return false; + } + } +} diff --git a/NetworkLibrary/DistributedP2P/SimpleRelay/PipeState.cs b/NetworkLibrary/DistributedP2P/SimpleRelay/PipeState.cs new file mode 100644 index 0000000..c53ac80 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/SimpleRelay/PipeState.cs @@ -0,0 +1,35 @@ +using NetworkLibrary.DistributedP2P.Server.StateManagement; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.SimpleRelay +{ + internal class PipeState + { + + internal List Clients = new List(); + internal PipeToken pipeData; + private object mtex = new object(); + + public PipeState(PipeToken pipeData) + { + this.pipeData = pipeData; + } + + internal bool IsComplete() + { + lock (mtex) + return Clients.Count > 1; + } + + internal void RegisterClient(T guid) + { + lock (mtex) + Clients.Add(guid); + } + + + } +} diff --git a/NetworkLibrary/DistributedP2P/SimpleRelay/RelayService.cs b/NetworkLibrary/DistributedP2P/SimpleRelay/RelayService.cs new file mode 100644 index 0000000..28801f9 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/SimpleRelay/RelayService.cs @@ -0,0 +1,577 @@ +//#define PRINT + +using NetworkLibrary.Components.Crypto.DigitalSignature; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.DistributedP2P.Server.StateManagement; +using NetworkLibrary.TCP.Base; +using NetworkLibrary.UDP; +using NetworkLibrary.Utils; +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +namespace NetworkLibrary.DistributedP2P.SimpleRelay +{ + internal class PipeToken + { + public Guid Token; + public DateTime Expiration; + + internal bool IsExpired() + { + DateTime now = DateTime.UtcNow; + if (now > Expiration) + return true; + + return false; + } + } + internal class TcpTokenStorage + { + internal byte[] TokenBytes = new byte[PipeData.TokenLength]; + int tokenOffset = 0; + + internal int StoreTokenFragment(byte[] tokenFragment, int offset, int count) + { + if (count > PipeData.TokenLength - tokenOffset) + { + return -1; + } + else + { + Buffer.BlockCopy(tokenFragment, offset, TokenBytes, tokenOffset, count); + tokenOffset += count; + if (tokenOffset == PipeData.TokenLength) + { + return 1; + } + return 0; + } + } + } + + internal class RelayService : IDisposable + { + int tokenLifetimeMs = 200000; + + internal AsyncTcpServer TcpServer; + internal AsyncUdpServer UdpServer; + + internal ConcurrentDictionary pipeMapTcp = new ConcurrentDictionary(); + internal ConcurrentDictionary pipeMapUdp = new ConcurrentDictionary(); + + private ConcurrentDictionary> activeTcpPipeStates = new ConcurrentDictionary>(); + private ConcurrentDictionary> activeUdpPipeStates = new ConcurrentDictionary>(); + + // Guid will be the id of coming peer, determined by relay. + //Room state will hold room id. + private ConcurrentDictionary activeRoomStates = new ConcurrentDictionary(); + private ConcurrentDictionary activeRooms = new ConcurrentDictionary(); + + private ConcurrentDictionary roomMapTcp = new ConcurrentDictionary(); + private ConcurrentDictionary roomMapUdp = new ConcurrentDictionary(); + + private readonly ConcurrentDictionary tokenStorage = new ConcurrentDictionary(); + + byte[] cryptoKey = new byte[32]; + + ThreadLocal signer = new ThreadLocal(); + readonly object tokenMtex = new object(); + + public RelayService(int TcpPort, int UdpPort) + { + var rng = RandomNumberGenerator.Create(); + rng.GetBytes(cryptoKey, 0, cryptoKey.Length); + + TcpServer = new AsyncTcpServer(TcpPort); + TcpServer.GatherConfig = ScatterGatherConfig.UseBuffer; + UdpServer = new AsyncUdpServer(UdpPort); + UdpServer.ClientDisconnected += HandleUdpPipeDisconnect; + + TcpServer.OnClientAccepted += TcpClientAccepted; + TcpServer.OnClientDisconnected += HandleTcpPipeDisconnect; + + TcpServer.OnBytesReceived += HandleTcpBytes; + UdpServer.OnBytesRecieved += HandleUdpBytes; + + UdpServer.StartServer(); + TcpServer.StartServer(); +#if PRINT + print(); +#endif + + } + + private PrivateKeySign GetSigner() + { + if (signer.Value == null) + { + signer.Value = new PrivateKeySign(cryptoKey); + } + return signer.Value; + } + + private async Task print() + { + while (true) + { + await Task.Delay(1000); + long s = Interlocked.Exchange(ref sent, 0); + Console.WriteLine($"Num clint {TcpServer.SessionCount} - Transfer rate:"+s.ToString("N1")); + } + } + + private void TcpClientAccepted(Guid guid) + { + TimerService.RegisterTimer(guid, tokenLifetimeMs, () => HandleTcpClientTimeout(guid)); + } + + private void HandleTcpClientTimeout(Guid guid) + { + if (!pipeMapTcp.ContainsKey(guid)) + { + tokenStorage.TryRemove(guid, out _); + TcpServer.CloseSession(guid); + // ddos here + } + } + + private void CancelTimeout(Guid guid) + { + TimerService.CancelTimeout(guid); + tokenStorage.TryRemove(guid, out _); + } + + + + private void HandleTcpBytes(Guid clientId, byte[] bytes, int offset, int count) + { + if (!RouteTcp(clientId, bytes, offset, count)) + { + ManagePipeToken(clientId, bytes, offset, count); + } + } + + private void HandleUdpBytes(IPEndPoint endpoint, byte[] bytes, int offset, int count) + { + if (!RouteUdp(endpoint, bytes, offset, count)) + { + ManagePipeToken(endpoint, bytes, offset, count); + } + } + + long sent; + private bool RouteTcp(Guid guid, byte[] bytes, int offset, int count) + { + // we should look for room stuff here + if (pipeMapTcp.TryGetValue(guid, out var to)) + { +#if PRINT + Interlocked.Add(ref sent, count); +#endif + + TcpServer.SendBytesToClientDirect(to, bytes, offset, count); + return true; + } + else if (roomMapTcp.TryGetValue(guid, out var room)) + { + room.HandleMessage(guid, bytes, offset, count); + return true; + } + return false; + } + + private bool RouteUdp(IPEndPoint endpoint, byte[] bytes, int offset, int count) + { + if (pipeMapUdp.TryGetValue(endpoint, out var to)) + { +#if PRINT + Interlocked.Add(ref sent, count); +#endif + UdpServer.SendBytesToClient(to, bytes, offset, count); + return true; + } + else if (roomMapUdp.TryGetValue(endpoint, out var room)) + { + room.HandleMessage(endpoint, bytes, offset, count); + return true; + } + return false; + } + + + //tcp + private void ManagePipeToken(Guid ephemeralId, byte[] bytes, int offset, int count) + { + // state object etc + lock (tokenMtex) + { + PipeToken token = null; + byte[] tokenBytes = null; + if (count == PipeData.TokenLength) + { + token = DeserializePipeToken(bytes, offset); + tokenBytes = ByteCopy.ToArray(bytes, offset, count); + } + else + { + + if (!tokenStorage.TryGetValue(ephemeralId, out var storage)) + { + storage = new TcpTokenStorage(); + tokenStorage.TryAdd(ephemeralId,storage); + + } + + int result = storage.StoreTokenFragment(bytes, offset, count); + + if (result == 0)//incomplete + return; + + if (result == -1)// nonsense + { + RemoveTcpClient(ephemeralId); + return; + } + if (result == 1)// tokenComplete + { + token = DeserializePipeToken(storage.TokenBytes, 0); + tokenBytes = storage.TokenBytes; + } + } + + if (activeTcpPipeStates.TryGetValue(token.Token, out var pipeState)) + { + if (VerifyToken(tokenBytes, token.Expiration)) + { + Console.WriteLine("TokenVerified"); + pipeState.RegisterClient(ephemeralId); + + if (pipeState.IsComplete()) + { + activeTcpPipeStates.TryRemove(token.Token, out _); + TcpPipeCreated(pipeState.Clients[0], pipeState.Clients[1]); + } + + TcpServer.SendBytesToClientDirect(ephemeralId, new byte[1] { 0x01 }, 0, 1); + } + else + { + Console.WriteLine("Token Rejected"); + RemoveTcpClient(ephemeralId); + } + } + else if (activeRoomStates.TryRemove(token.Token, out var roomState))//token is peerId + { + if (VerifyToken(tokenBytes, token.Expiration)) + { + if (roomState.Verify(token, ephemeralId)) + { + if (activeRooms.TryGetValue(roomState.RoomId, out Room room)) + { + room.Add(token.Token, roomState); + TcpServer.SendBytesToClientDirect(ephemeralId, new byte[1] { 0x01 }, 0, 1); + } + } + } + else + { + RemoveTcpClient(ephemeralId); + } + + } + else + { + RemoveTcpClient(ephemeralId); + } + + + } + } + + //udp + private void ManagePipeToken(IPEndPoint clientEp, byte[] bytes, int offset, int count) + { + lock (tokenMtex) + { + byte[] tokenBytes = ByteCopy.ToArray(bytes, offset, count); + + if (tokenBytes.Length != PipeData.TokenLength) + { + return; + } + + PipeToken token = DeserializePipeToken(tokenBytes, 0); + + + if (activeUdpPipeStates.TryGetValue(token.Token, out var pipeState)) + { + if (VerifyToken(tokenBytes, token.Expiration)) + { + Console.WriteLine("Token verified"); + pipeState.RegisterClient(clientEp); + + if (pipeState.IsComplete()) + { + activeUdpPipeStates.TryRemove(token.Token, out _); + UdpPipeCreated(pipeState.Clients[0], pipeState.Clients[1]); + } + + UdpServer.SendBytesToClient(clientEp, new byte[1] { 0x01 }, 0, 1); + } + else + { + Console.WriteLine("Token rejected"); + // ddos here. + UdpServer.RemoveClient(clientEp); + } + } + else if (activeRoomStates.TryRemove(token.Token, out var roomState))//token is peerId + { + if (VerifyToken(tokenBytes, token.Expiration)) + { + if (roomState.Verify(token, clientEp)) + { + if (activeRooms.TryGetValue(roomState.RoomId, out Room room)) + { + room.Add(token.Token, roomState); + UdpServer.SendBytesToClient(clientEp, new byte[1] { 0x01 }, 0, 1); + + } + } + } + else + { + UdpServer.RemoveClient(clientEp); + } + } + + else + { + UdpServer.RemoveClient(clientEp); + } + } + + } + + private bool VerifyToken(byte[] Token, DateTime expiration) + { + + var calculatedSignature = GetSigner().Sign(Token, 0, 24); + + if (SignatureMatch(calculatedSignature, Token)) + { + if (DateTime.UtcNow <= expiration) + return true; + return false; + } + else + { + Console.WriteLine("Signature did not match"); + return false; + } + } + + private bool SignatureMatch(byte[] localSignature, byte[] incomingSignature) + { + for (int i = 0; i < 32; i++) + { + if (localSignature[i] != incomingSignature[24 + i]) + return false; + } + return true; + } + + + private PipeToken DeserializePipeToken(byte[] bytes, int offset) + { + var token = PrimitiveEncoder.ReadGuid(bytes, ref offset);//16 + var expiration = DateTime.FromBinary(PrimitiveEncoder.ReadFixedInt64(bytes, ref offset));//8 + + return new PipeToken() { Token = token, Expiration = expiration }; + } + + private void RemoveTcpClient(Guid guid) + { + TcpServer.CloseSession(guid); + } + + private void TcpPipeCreated(Guid from, Guid to) + { + pipeMapTcp.TryAdd(from, to); + pipeMapTcp.TryAdd(to, from); + + CancelTimeout(from); + CancelTimeout(to); + } + + private void UdpPipeCreated(IPEndPoint from, IPEndPoint to) + { + pipeMapUdp.TryAdd(from, to); + pipeMapUdp.TryAdd(to, from); + } + + private void HandleUdpPipeDisconnect(IPEndPoint from) + { + if (pipeMapUdp.TryRemove(from, out var to)) + { + pipeMapUdp.TryRemove(to, out _); + } + else if (roomMapUdp.TryRemove(from, out Room room)) + { + room.HandleDisconnect(from); + } + } + + private void HandleTcpPipeDisconnect(Guid from) + { + if (pipeMapTcp.TryRemove(from, out Guid to)) + { + TcpServer.CloseSession(to); + pipeMapTcp.TryRemove(to, out _); + } + else if (roomMapTcp.TryRemove(from, out Room room)) + { + room.HandleDisconnect(from); + } + + tokenStorage.TryRemove(from, out _); + } + + + + // for direct p2p + public byte[] GetPipeToken(bool tcp) + { + byte[] data; + PipeToken pipeData; + Createtoken(Guid.NewGuid(), out data, out pipeData); + + if (tcp) + RegisterTcpToken(pipeData); + else + RegisterUdpToken(pipeData); + + return data; + } + private void Createtoken(Guid tokenId, out byte[] data, out PipeToken pipeData) + { + data = new byte[PipeData.TokenLength]; + int offset = 0; + pipeData = new PipeToken(); + pipeData.Token = tokenId; + pipeData.Expiration = DateTime.UtcNow.AddMilliseconds(tokenLifetimeMs); + + PrimitiveEncoder.WriteGuid(data, ref offset, pipeData.Token);//16 + + long Time = pipeData.Expiration.ToBinary(); + PrimitiveEncoder.WriteFixedInt64(data, ref offset, Time); //8 + var signature = GetSigner().Sign(data, 0, 24); + Buffer.BlockCopy(signature, 0, data, offset, 32);//32 + } + + + private void RegisterTcpToken(PipeToken pipeData) + { + if (!activeTcpPipeStates.TryAdd(pipeData.Token, new PipeState(pipeData))) + Console.WriteLine("GuidDupe"); + TimerService.RegisterTimer(pipeData.Token, tokenLifetimeMs, () => activeTcpPipeStates.TryRemove(pipeData.Token, out _)); + } + + private void RegisterUdpToken(PipeToken pipeData) + { + activeUdpPipeStates.TryAdd(pipeData.Token, new PipeState(pipeData)); + TimerService.RegisterTimer(pipeData.Token, tokenLifetimeMs, () => activeUdpPipeStates.TryRemove(pipeData.Token, out _)); + + } + + + //for broadcast + public bool CreateRoom(Guid roomId, RoomProtocol protocol) + { + Room room = new Room(roomId, protocol); + room.PeerRegistered += HandleRoomPeerRegistered; + room.PeerLeft += HandleRoomPeerLeft; + room.SendMessage += RouteRoomMessage; + room.RoomDestroyed += () => activeRooms.TryRemove(room.roomId, out _); + + bool good = activeRooms.TryAdd(roomId, room); + if (good) + { + return true; + } + else + { + room.Clear(); + return false; + } + } + + + // get token for spesific peer, who wants to join a room + public byte[] GetRoomToken(Guid roomId, Guid peerId) + { + if (activeRooms.TryGetValue(roomId, out var room)) + { + PeerRoomState state = new PeerRoomState(); + state.RoomId = roomId; + state.ExpectedClient = peerId; + + Createtoken(peerId, out byte[] data, out PipeToken token); + + if (activeRoomStates.TryAdd(peerId, state)) + return data; + + return null; + } + return null; + } + + public void RemovePeerFromRoom(Guid roomId, Guid peerId) + { + if (activeRooms.TryGetValue(roomId, out var room)) + { + room.RemovePeer(peerId); + + } + } + + private void RouteRoomMessage(PeerRoomState to, byte[] b, int o, int c) + { + if (to.isTcp) + { + TcpServer.SendBytesToClientDirect(to.EphemeralId, b, o, c); + } + else + { + UdpServer.SendBytesToClient(to.AssociatedEndpoint, b, o, c); + } + } + + private void HandleRoomPeerLeft(PeerRoomState state) + { + if (state.isTcp) + { + roomMapTcp.TryRemove(state.EphemeralId, out _); + } + else + { + roomMapUdp.TryRemove(state.AssociatedEndpoint, out _); + } + } + + private void HandleRoomPeerRegistered(PeerRoomState state) + { + throw new NotImplementedException(); + } + public void Dispose() + { + TcpServer.ShutdownServer(); + UdpServer.Dispose(); + } + } + + +} diff --git a/NetworkLibrary/DistributedP2P/SimpleRelay/Room.cs b/NetworkLibrary/DistributedP2P/SimpleRelay/Room.cs new file mode 100644 index 0000000..c81b419 --- /dev/null +++ b/NetworkLibrary/DistributedP2P/SimpleRelay/Room.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace NetworkLibrary.DistributedP2P.SimpleRelay +{ + public enum RoomProtocol + { + Tcp, + Udp, + } + internal class Room + { + public Guid roomId; + public readonly RoomProtocol protocol; + public Action PeerRegistered; + public Action PeerLeft; + public Action SendMessage; + public Action RoomDestroyed; + + //peerId -> info + ConcurrentDictionary roster = new ConcurrentDictionary(); + + public Room(Guid roomId, RoomProtocol protocol) + { + this.roomId = roomId; + this.protocol = protocol; + } + + // peer id determined by true server + internal void Add(Guid peerId, PeerRoomState roomState) + { + if( roster.TryAdd(peerId, roomState)) + { + PeerRegistered?.Invoke(roomState); + } + } + + internal void HandleMessage(Guid from, byte[] bytes, int offset, int count) + { + foreach (var peerInfo in roster.Values) + { + if (peerInfo.EphemeralId.Equals(from)) + continue; + + SendMessage?.Invoke(peerInfo, bytes, offset, count); + } + } + + internal void HandleMessage(IPEndPoint from, byte[] bytes, int offset, int count) + { + foreach (var peerInfo in roster.Values) + { + if (peerInfo.AssociatedEndpoint.Equals(from)) + continue; + + SendMessage?.Invoke(peerInfo, bytes, offset, count); + } + } + + internal bool RemovePeer(Guid peerId) + { + bool removed = roster.TryRemove(peerId, out var st); + if (removed) + { + PeerLeft?.Invoke(st); + + if (roster.Count == 0) + { + RoomDestroyed?.Invoke(); + } + } + + return removed; + } + + internal bool HandleDisconnect(IPEndPoint peerEndpoint) + { + foreach (var item in roster) + { + if (item.Value.AssociatedEndpoint.Equals(peerEndpoint)) + { + RemovePeer(item.Key); + return true; + } + } + return false; + } + + internal bool HandleDisconnect(Guid ephemeralId) + { + foreach (var item in roster) + { + if (item.Value.EphemeralId.Equals(ephemeralId)) + { + RemovePeer(item.Key); + return true; + } + } + return false; + } + + public void Clear() + { + PeerRegistered = null; + PeerLeft = null; + SendMessage = null; + RoomDestroyed = null; + } + + //internal IPEndPoint GetEndpoint(Guid to) + //{ + // roster.TryGetValue(to, out PeerRoomState state); + // return state.AssociatedEndpoint; + //} + + + } +} diff --git a/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageServer.cs b/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageServer.cs index 132c7c2..6ce15eb 100644 --- a/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageServer.cs +++ b/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageServer.cs @@ -135,6 +135,7 @@ public Task SendMessageAndWaitResponse(Guid clientId, MessageEn SendAsyncMessage(clientId, message); return task; } + public IPEndPoint GetIPEndPoint(Guid cliendId) { return GetSessionEndpoint(cliendId); diff --git a/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageSession.cs b/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageSession.cs index bfee4b1..51454d2 100644 --- a/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageSession.cs +++ b/NetworkLibrary/MessageProtocol/Fast/Network/SecureMessageSession.cs @@ -51,7 +51,7 @@ public void SendAsync(MessageEnvelope message) { SendAsyncInternal(message); } - catch { if (!IsSessionClosing()) throw; } + catch { if (!IsSessionClosing()) { EndSession(); throw; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -97,7 +97,7 @@ public void SendAsync(MessageEnvelope envelope, T message) { SendAsyncInternal(envelope, message); } - catch { if (!IsSessionClosing()) throw; } + catch { if (!IsSessionClosing()) { EndSession(); throw; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -132,8 +132,6 @@ private void SendAsyncInternal(MessageEnvelope envelope, T message) return; } - //mq.TryFlushQueue(ref sendBuffer, 0, out int amountWritten); - //WriteOnSessionStream(amountWritten); FlushAndSend(); } @@ -147,7 +145,7 @@ public void SendAsync(MessageEnvelope envelope, Action seria { SendAsyncInternal(envelope, serializationCallback); } - catch { if (!IsSessionClosing()) throw; } + catch { if (!IsSessionClosing()) { EndSession(); throw; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -182,10 +180,9 @@ private void SendAsyncInternal(MessageEnvelope envelope, Actionhttps://github.com/ReferenceType/StandardNetworkLibrary RefrenceType Apache 2.0 - 3.0.0 + 3.1.0 2.0.1 2.0.1 For release notes check out: https://github.com/ReferenceType/StandardNetworkLibrary diff --git a/NetworkLibrary/P2P/Components/HolePunch/KnownTypeSerializer.cs b/NetworkLibrary/P2P/Components/HolePunch/KnownTypeSerializer.cs index fa3876a..1ea4250 100644 --- a/NetworkLibrary/P2P/Components/HolePunch/KnownTypeSerializer.cs +++ b/NetworkLibrary/P2P/Components/HolePunch/KnownTypeSerializer.cs @@ -1,5 +1,8 @@ using NetworkLibrary.Components; -using NetworkLibrary.P2P.Generic; +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Client.StateManagement; +using NetworkLibrary.DistributedP2P.Server; +using NetworkLibrary.DistributedP2P.Server.StateManagement; using NetworkLibrary.Utils; using System; using System.Collections.Generic; @@ -8,6 +11,282 @@ namespace NetworkLibrary.P2P.Components.HolePunch { public class KnownTypeSerializer { + #region ClientPipeData + internal static void SerializeClientPipeData(PooledMemoryStream stream, ClientPipeData hpData) + { + byte index = 0; + int oldPos = stream.Position32; + stream.WriteByte(index); + + if (hpData.ChannelInfo != null) + { + SerializeChannelInfo(stream, hpData.ChannelInfo); + index = 1; + + } + if (hpData.DHPublic != null) + { + PrimitiveEncoder.WriteInt32(stream, hpData.DHPublic.Length); + stream.Write(hpData.DHPublic, 0, hpData.DHPublic.Length); + index += 2; + } + + var buf = stream.GetBuffer(); + buf[oldPos] = index; + + } + + + internal static ClientPipeData DeserializeClientPipeData(byte[] buffer, ref int offset) + { + var hpData = new ClientPipeData(); + var index = buffer[offset++]; + + + if ((index & 1) != 0) + { + hpData.ChannelInfo = DeserializeChannelInfo(buffer, ref offset); + } + if ((index & 1 << 1) != 0) + { + int Dhlen = PrimitiveEncoder.ReadInt32(buffer, ref offset); + if (Dhlen > 0) + { + hpData.DHPublic = ByteCopy.ToArray(buffer, offset, Dhlen); + offset += Dhlen; + } + else + hpData.DHPublic = new byte[0]; + } + + return hpData; + } + #endregion + + #region ClientHolepunchData + + internal static void SerializeHolepunchData(PooledMemoryStream stream, ClientHolepunchData hpData) + { + byte index = 0; + int oldPos = stream.Position32; + stream.WriteByte(index); + + if (hpData.ChannelInfo != null) + { + SerializeChannelInfo(stream, hpData.ChannelInfo); + index = 1; + + } + if (hpData.Endpoints != null) + { + SerializeEndpointTransferMessage(stream, hpData.Endpoints); + index += 2; + } + + if (hpData.DHPublic != null) + { + PrimitiveEncoder.WriteInt32(stream, hpData.DHPublic.Length); + stream.Write(hpData.DHPublic, 0, hpData.DHPublic.Length); + index += 4; + } + + var buf = stream.GetBuffer(); + buf[oldPos] = index; + + } + + + internal static ClientHolepunchData DeserializeHolepunchData(byte[] buffer, ref int offset) + { + var hpData = new ClientHolepunchData(); + var index = buffer[offset++]; + + + if ((index & 1) != 0) + { + hpData.ChannelInfo = DeserializeChannelInfo(buffer, ref offset); + } + if ((index & 1 << 1) != 0) + { + hpData.Endpoints = DeserializeEndpointTransferMessage(buffer, ref offset); + } + if ((index & 1 << 2) != 0) + { + int Dhlen = PrimitiveEncoder.ReadInt32(buffer, ref offset); + if (Dhlen > 0) + { + hpData.DHPublic = ByteCopy.ToArray(buffer, offset, Dhlen); + offset += Dhlen; + } + else + hpData.DHPublic = new byte[0]; + } + + return hpData; + } + + #endregion + + #region ChannelInfo + + public static void SerializeChannelInfo(PooledMemoryStream stream, ChannelInfo channelInfo) + { + PrimitiveEncoder.WriteInt32(stream, (int)channelInfo.ChannelType); + PrimitiveEncoder.WriteStringUtf8(stream, channelInfo.ChannelName ?? ""); + } + + + public static ChannelInfo DeserializeChannelInfo(byte[] buffer, ref int offset) + { + return new ChannelInfo() + { + ChannelType = (ChannelType)PrimitiveEncoder.ReadInt32(buffer, ref offset), + ChannelName = PrimitiveEncoder.ReadStringUtf8(buffer, ref offset), + }; + } + #endregion + + #region PeerStatusList + internal static void SerializePeerStatusList(PooledMemoryStream stream, PeerStatusList statusList) + { + byte index = 0; + // Reserve for index + int oldPos = stream.Position32; + stream.WriteByte(index); + + if (statusList.NewOnline.Count > 0) + { + PrimitiveEncoder.WriteInt32(stream, statusList.NewOnline.Count); + foreach (var item in statusList.NewOnline) + { + SerializePeerStatus(stream, item.Value); + } + index = 1; + } + if (statusList.WentOffline.Count > 0) + { + PrimitiveEncoder.WriteInt32(stream, statusList.WentOffline.Count); + foreach (var item in statusList.WentOffline) + { + SerializePeerStatus(stream, item.Value); + } + index += 2; + } + + var buf = stream.GetBuffer(); + buf[oldPos] = index; + } + + internal static PeerStatusList DeserializePeerStatusList(byte[] buffer, ref int offset) + { + var index = buffer[offset++]; + + var data = new PeerStatusList(); + if ((index & 1) != 0) + { + int len = PrimitiveEncoder.ReadInt32(buffer, ref offset); + for (int i = 0; i < len; i++) + { + //online + var sts = DeserializePeerStatus(buffer, ref offset); + data.NewOnline.TryAdd(sts.EphemeralId, sts); + } + + } + + + if ((index & 1 << 1) != 0) + { + int len = PrimitiveEncoder.ReadInt32(buffer, ref offset); + for (int i = 0; i < len; i++) + { + //offline + var sts = DeserializePeerStatus(buffer, ref offset); + data.WentOffline.TryAdd(sts.EphemeralId, sts); + } + } + + return data; + } + + internal static void SerializePeerStatus(PooledMemoryStream stream, PeerStatus status) + { + PrimitiveEncoder.WriteGuid(stream, status.EphemeralId); + PrimitiveEncoder.WriteGuid(stream, status.PeerId); + PrimitiveEncoder.WriteDatetime(stream, status.OnlineSince); + } + + internal static PeerStatus DeserializePeerStatus(byte[] buffer, ref int offset) + { + PeerStatus peerStatus = new PeerStatus(); + peerStatus.EphemeralId = PrimitiveEncoder.ReadGuid(buffer, ref offset); + peerStatus.PeerId = PrimitiveEncoder.ReadGuid(buffer, ref offset); + peerStatus.OnlineSince = PrimitiveEncoder.ReadDatetime(buffer, ref offset); + + return peerStatus; + } + + + #endregion + + #region PipeData + + internal static void SerializePipeData(PooledMemoryStream stream, PipeData pipeData) + { + byte index = 0; + int oldPos = stream.Position32; + stream.WriteByte(index); + + if (pipeData.Token != null) + { + PrimitiveEncoder.WriteInt32(stream, PipeData.TokenLength); + stream.Write(pipeData.Token, 0, PipeData.TokenLength); + index = 1; + } + if (pipeData.DHPublic != null) + { + PrimitiveEncoder.WriteInt32(stream, pipeData.DHPublic.Length); + stream.Write(pipeData.DHPublic, 0, pipeData.DHPublic.Length); + index += 2; + } + if (pipeData.PipeEndpoint != null) + { + SerializeEndpointData(stream, pipeData.PipeEndpoint); + index += 4; + } + + var buf = stream.GetBuffer(); + buf[oldPos] = index; + } + + internal static PipeData DeserializePipeData(byte[] buffer, ref int offset) + { + PipeData pipeData = new PipeData(); + var index = buffer[offset++]; + + if ((index & 1) != 0) + { + int len = PrimitiveEncoder.ReadInt32(buffer, ref offset); + pipeData.Token = ByteCopy.ToArray(buffer, offset, len); + offset += len; + } + if ((index & 1 << 1) != 0) + { + int len = PrimitiveEncoder.ReadInt32(buffer, ref offset); + pipeData.DHPublic = ByteCopy.ToArray(buffer, offset, len); + offset += len; + } + if ((index & 1 << 2) != 0) + { + pipeData.PipeEndpoint = DeserializeEndpointData(buffer, ref offset); + } + + return pipeData; + } + + + #endregion + #region Endpoint Data public static void SerializeEndpointData(PooledMemoryStream stream, EndpointData data) { @@ -107,6 +386,37 @@ public static EndpointTransferMessage DeserializeEndpointTransferMessage(byte[] } + if ((index & 1 << 1) != 0) + { + data.PortRemote = PrimitiveEncoder.ReadInt32(buffer, ref offset); + } + + if ((index & 1 << 2) != 0) + { + int listCount = PrimitiveEncoder.ReadInt32(buffer, ref offset); + data.LocalEndpoints = new List(listCount + 1); + for (int i = 0; i < listCount; i++) + { + data.LocalEndpoints.Add(DeserializeEndpointData(buffer, ref offset)); + } + } + + return data; + } + + public static EndpointTransferMessage DeserializeEndpointTransferMessage(byte[] buffer, ref int offset) + { + var index = buffer[offset++]; + + var data = new EndpointTransferMessage(); + if ((index & 1) != 0) + { + int len = PrimitiveEncoder.ReadInt32(buffer, ref offset); + data.IpRemote = ByteCopy.ToArray(buffer, offset, len); + offset += len; + } + + if ((index & 1 << 1) != 0) { data.PortRemote = PrimitiveEncoder.ReadInt32(buffer, ref offset); @@ -145,6 +455,11 @@ public static void SerializePeerInfo(PooledMemoryStream stream, PeerInfo data) stream.WriteUshort(data.Port); index += 2; } + if (data.RegisteryTime != default) + { + PrimitiveEncoder.WriteDouble(stream, data.RegisteryTime); + index += 4; + } var buf = stream.GetBuffer(); buf[oldPos] = index; @@ -170,6 +485,11 @@ public static PeerInfo DeserializePeerInfo(byte[] buffer, ref int offset) offset += 2; } + if ((index & 1 << 2) != 0) + { + data.RegisteryTime = PrimitiveEncoder.ReadDouble(buffer, ref offset); + } + return data; } #endregion diff --git a/NetworkLibrary/P2P/Components/HolePunch/Messages.cs b/NetworkLibrary/P2P/Components/HolePunch/Messages.cs index 54bdcd1..d136682 100644 --- a/NetworkLibrary/P2P/Components/HolePunch/Messages.cs +++ b/NetworkLibrary/P2P/Components/HolePunch/Messages.cs @@ -29,8 +29,15 @@ public EndpointData() public EndpointData(IPEndPoint ep) { - Ip = ep.Address.GetAddressBytes(); + Ip = ep.Address.MapToIPv4().GetAddressBytes(); Port = ep.Port; } + + public EndpointData(string ip, int port) + { + IPAddress add = IPAddress.Parse(ip); + Ip = add.GetAddressBytes(); + this.Port = port; + } } } diff --git a/NetworkLibrary/P2P/Components/InfoMessages.cs b/NetworkLibrary/P2P/Components/InfoMessages.cs index 189e934..bbc4387 100644 --- a/NetworkLibrary/P2P/Components/InfoMessages.cs +++ b/NetworkLibrary/P2P/Components/InfoMessages.cs @@ -12,6 +12,7 @@ public class PeerInfo { public byte[] Address { get; set; } public ushort Port { get; set; } + public double RegisteryTime { get; set; } } public class RoomPeerList diff --git a/NetworkLibrary/P2P/Constants.cs b/NetworkLibrary/P2P/Constants.cs index 03c2d90..3c8347d 100644 --- a/NetworkLibrary/P2P/Constants.cs +++ b/NetworkLibrary/P2P/Constants.cs @@ -19,9 +19,11 @@ public class Constants public const string Ping = "Ping"; public const string Pong = "Pong"; + public const string KeepAlieve = ".K"; public const string RoomUpdate = "12"; + public const string RoomAvalibalilityUpdate = "12a"; public const string JoinRoom = "13"; public const string LeaveRoom = "14"; public const string GetAvailableRooms = "15"; @@ -32,7 +34,9 @@ public class Constants public const string Judp = "20"; // key of a dict, picked some unusual control char. - public const string RoomName = "\v"; + public const string RoomBroadcast = "\v"; + public const string RoomBroadcastBatched = "\t"; + public const string TimeSync = "ntp"; public const string ReqTCPHP = "ReqTCPHP"; public const string TcpPortMap = "TcpPortMap"; diff --git a/NetworkLibrary/P2P/Generic/ClientPeerData.cs b/NetworkLibrary/P2P/Generic/ClientPeerData.cs new file mode 100644 index 0000000..105fd16 --- /dev/null +++ b/NetworkLibrary/P2P/Generic/ClientPeerData.cs @@ -0,0 +1,30 @@ +using NetworkLibrary.P2P.Components.Modules; +using System; +using System.Collections.Generic; +using System.Text; +using NetworkLibrary.UDP.Jumbo; +using System.Collections.Concurrent; +using NetworkLibrary.MessageProtocol; + +namespace NetworkLibrary.P2P.Generic +{ + public class ClientPeerData where S : ISerializer, new() + { + internal PeerInformation PeerInfo; + internal List RUdpModules = new List(); + internal JumboModule JumboUdpModule; + internal EndpointGroup punchedEndpoints; + internal AesTcpModule punchedTcpModule; + + internal void Clear() + { + foreach (var item in RUdpModules) + { + item?.Release(); + } + JumboUdpModule.Release(); + + punchedTcpModule.Dispose(); + } + } +} diff --git a/NetworkLibrary/P2P/Generic/RelayClientBase.cs b/NetworkLibrary/P2P/Generic/RelayClientBase.cs index d0964e2..546b5a8 100644 --- a/NetworkLibrary/P2P/Generic/RelayClientBase.cs +++ b/NetworkLibrary/P2P/Generic/RelayClientBase.cs @@ -14,7 +14,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Net; using System.Net.Security; using System.Net.Sockets; @@ -87,6 +90,8 @@ private set } } + public AesMode AESMode = AesMode.GCM; + public ConcurrentDictionary Peers = new ConcurrentDictionary(); internal ConcurrentDictionary PeerInfos { get; private set; } = new ConcurrentDictionary(); internal ConcurrentDictionary> RUdpModules = new ConcurrentDictionary>(); @@ -111,10 +116,13 @@ private set private ConcurrentDictionary> punchedTcpModules = new ConcurrentDictionary>(); internal ConcurrentDictionary peerCryptos = new ConcurrentDictionary(); private ClientStateManager clientStateManager; - public AesMode AESMode = AesMode.GCM; private X509Certificate2 clientCert; private int udpPort = 0; private bool initialised = false; + private Stopwatch clientClock = new Stopwatch(); + double timeOffset; + TimeSpan timeOffsetd; + long syncCount = 0; public RelayClientBase(X509Certificate2 clientCert, int udpPort = 0) { if (clientCert == null) @@ -141,6 +149,218 @@ private void Initialise() udpServer.SocketSendBufferSize = 12800000; udpServer.OnBytesRecieved += HandleUdpBytesReceived; udpServer.StartServer(); + + clientClock.Start(); + } + public double GetTime() + { + return clientClock.Elapsed.TotalMilliseconds + timeOffset; + } + + public DateTime GetDateTime() + { + return DateTime.UtcNow.Add(timeOffsetd); + } + + AsyncDispatcher timesyncOperation; + public void StartAutoTimeSync(int periodMs,bool disconnectOnTimeout,bool usePTP = false) + { + Interlocked.Exchange(ref timesyncOperation, new AsyncDispatcher())?.Abort(); + int failureCount = 0; + timesyncOperation.LoopPeriodicTask(async () => + { + try + { + if (Interlocked.CompareExchange(ref disposed, 0, 0) == 0) + { + if (IsConnected) + { + bool result = await SyncTime(usePTP).ConfigureAwait(false); + if (disconnectOnTimeout && result == false ) + { + if (++failureCount > 2) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "TimeSync operation timed out "); + Disconnect(); + } + + } + else + { + failureCount = 0; + } + } + + } + else + { + timesyncOperation.Abort(); + } + } + catch(Exception e) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "TimeSync operation failed with exception " + e.Message); + Disconnect(); + } + + }, periodMs).ConfigureAwait(false); + } + + public void StopAutoTimeSync() + { + timesyncOperation?.Abort(); + } + + + List timesHistory = new List(); + List timesHistoryd = new List(); + private static readonly SemaphoreSlim asyncLock = new SemaphoreSlim(1, 1); + public async Task SyncTime(bool usePtp = false) + { + try + { + await asyncLock.WaitAsync().ConfigureAwait(false); + + if (!IsConnected) + return false; + + int sampleSize = 3; + var sCnt = Interlocked.CompareExchange(ref syncCount, 0, 0); + + if (sCnt == 0) + sampleSize = 12; + + if (sCnt > 50) + sampleSize = 2; + + if (sCnt > 100) + sampleSize = 1; + + + for (int i = 0; i < sampleSize; i++) + { + var result = usePtp?await GetOffsetPTP().ConfigureAwait(false): await GetOffsetNTP().ConfigureAwait(false); + if (result.Succes) + { + timesHistory.Add(result.PreciseTime); + timesHistoryd.Add(result.DateTimeOffset); + } + else return false; + } + + if(timesHistory.Count<4) + return false; + + var times = Statistics.FilterOutliers(timesHistory); + var timesd = Statistics.FilterOutliers(timesHistoryd); + if (timesHistory.Count > 600) + { + timesHistory = timesHistory.Skip(60).ToList(); + timesHistoryd = timesHistoryd.Skip(60).ToList(); + } + + double average = times.Sum() / times.Count(); + double averaged = timesd.Sum(ts => ts.Ticks) / timesd.Count(); + timeOffset = average; + + if (sCnt > 0) + { + var calculatedAvg = TimeSpan.FromTicks((long)averaged); + if (timeOffsetd < calculatedAvg) + { + timeOffsetd = calculatedAvg; + } + } + else + { + timeOffsetd = TimeSpan.FromTicks((long)averaged); + } + + Interlocked.Increment(ref syncCount); + return true; + + } + finally + { + asyncLock.Release(); + } + + } + class TimeResult { public double PreciseTime; public bool Succes; public DateTime ServerUTC; public TimeSpan DateTimeOffset; } + private async Task GetOffsetNTP() + { + var msg = new MessageEnvelope() + { + Header = Constants.TimeSync, + IsInternal = true, + }; + var now = clientClock.Elapsed.TotalMilliseconds; + var nowd = DateTime.UtcNow; + + var response = await tcpMessageClient.SendMessageAndWaitResponse(msg, 10000).ConfigureAwait(false); + if (response.Header != MessageEnvelope.RequestTimeout) + { + + var serverTime = PrimitiveEncoder.ReadFixedDouble(response.Payload, response.PayloadOffset); + var serverTimed = response.TimeStamp; + + var now1 = clientClock.Elapsed.TotalMilliseconds; + var now1d = DateTime.UtcNow; + + var timeOffset = ((serverTime - now) + (serverTime - now1)) / 2; + var timeOffsetd = ((serverTimed - nowd) + (serverTimed - now1d)).TotalMilliseconds / 2; + TimeSpan offd = TimeSpan.FromMilliseconds(timeOffsetd); + + return new TimeResult() { PreciseTime = timeOffset, DateTimeOffset = offd, Succes = true }; + } + return new TimeResult(); + + } + + private async Task GetOffsetPTP() + { + + var t1 = await GetServerTime(); + if (t1.Succes) + { + var t2 = clientClock.Elapsed.TotalMilliseconds; + var t2d = DateTime.UtcNow; + + var t3 = t2; + var t3d = t2d; + + var t4 = await GetServerTime(); + + if (t4.Succes) + { + double offset = ( ((t4.PreciseTime - t3) - (t2 - t1.PreciseTime)) / 2); + double offsetd = ( ((t4.ServerUTC - t3d) - (t2d - t1.ServerUTC)).TotalMilliseconds / 2); + TimeSpan offd =TimeSpan.FromMilliseconds(offsetd); + return new TimeResult() { PreciseTime = offset, DateTimeOffset = offd, Succes = true }; + } + else + return new TimeResult(); + } + else + return new TimeResult(); + + } + + private async Task GetServerTime() + { + var msg = new MessageEnvelope() + { + Header = Constants.TimeSync, + IsInternal = true, + }; + var response = await tcpMessageClient.SendMessageAndWaitResponse(msg, 10000); + if (response.Header != MessageEnvelope.RequestTimeout) + { + var serverTime = PrimitiveEncoder.ReadFixedDouble(response.Payload, response.PayloadOffset); + return new TimeResult() { PreciseTime = serverTime, ServerUTC = response.TimeStamp, Succes = true }; + } + return new TimeResult(); + } public void GetTcpStatistics(out TcpStatistics stats) => tcpMessageClient.GetStatistics(out stats); @@ -218,7 +438,15 @@ public async Task ConnectAsync(string host, int port) relayServerEndpoint = new IPEndPoint(IPAddress.Parse(connectHost), connectPort); - await tcpMessageClient.ConnectAsync(host, port).ConfigureAwait(false); + bool result = await tcpMessageClient.ConnectAsync(host, port).ConfigureAwait(false); + if (!result) + { + throw new TimeoutException(); + } + else + { + MiniLogger.Log(MiniLogger.LogLevel.Info, "Connection established"); + } var stateCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); clientStateManager.CreateConnectionState() @@ -241,8 +469,10 @@ public async Task ConnectAsync(string host, int port) IsConnected = true; pinger.PeerRegistered(sessionId); + timesHistory.Clear(); + syncCount = 0; + stateCompletion.SetResult(true); - } else stateCompletion.TrySetException(new TimeoutException()); }; @@ -307,7 +537,7 @@ private void HandleDisconnect() Peers.Clear(); peerCryptos.Clear(); - punchedEndpoints.Clear(); + ClearPunchedEndpoints(); foreach (var item in RUdpModules) { @@ -374,39 +604,48 @@ private async void SendPing(int intervalMs, bool sendToServer, bool sendTcpToPee MessageEnvelope msg = new MessageEnvelope(); msg.Header = Constants.Ping; - if (IsConnected) + try { - var time = DateTime.Now; - - if (sendToServer) + if (IsConnected) { - msg.From = sessionId; - msg.To = sessionId; + var time = DateTime.Now; - tcpMessageClient.SendAsyncMessage(msg); - pinger.NotifyTcpPingSent(sessionId, time); + if (sendToServer) + { + msg.From = sessionId; + msg.To = sessionId; - SendUdpMessage(sessionId, msg); - pinger.NotifyUdpPingSent(sessionId, time); - } + tcpMessageClient.SendAsyncMessage(msg); + pinger.NotifyTcpPingSent(sessionId, time); - time = DateTime.Now; + SendUdpMessage(sessionId, msg); + pinger.NotifyUdpPingSent(sessionId, time); + } + + time = DateTime.Now; - if (sendTcpToPeers) - BroadcastMessage(msg); - if (sendUdpToPeers) - BroadcastUdpMessage(msg); - foreach (var peer in Peers.Keys) - { if (sendTcpToPeers) - pinger.NotifyTcpPingSent(peer, time); + BroadcastMessage(msg); if (sendUdpToPeers) + BroadcastUdpMessage(msg); + foreach (var peer in Peers.Keys) { - //SendUdpMesssage(peer, msg); - pinger.NotifyUdpPingSent(peer, time); - } + if (sendTcpToPeers) + pinger.NotifyTcpPingSent(peer, time); + if (sendUdpToPeers) + { + //SendUdpMesssage(peer, msg); + pinger.NotifyUdpPingSent(peer, time); + } + } } + + } + catch + { + Disconnect(); + return; } } @@ -427,7 +666,7 @@ private void HandlePing(MessageEnvelope message, bool isTcp = true) if (isTcp) { message.To = message.From; - if(punchedTcpModules.TryGetValue(message.From,out var module)) + if(TryGetAesTcpModule(message.From,out var module)) { message.From = sessionId; @@ -473,13 +712,10 @@ public Dictionary GetUdpPingStatus() #endregion Ping - #region Send - - private void SendUdpMesssageInternal(Guid toId, MessageEnvelope message) + #region Wrap + private void GetUdpSendParams(Guid toId, out ConcurrentAesAlgorithm algo, out IPEndPoint endpoint) { - ConcurrentAesAlgorithm algo; - IPEndPoint endpoint; - if (punchedEndpoints.TryGetValue(toId, out EndpointGroup endpoints)) + if (TryGetTraversedEndpoint(toId, out var endpoints)) { endpoint = endpoints.ToSend; algo = peerCryptos[endpoint].algorithm; @@ -488,28 +724,105 @@ private void SendUdpMesssageInternal(Guid toId, MessageEnvelope message) { endpoint = relayServerEndpoint; algo = udpEncryiptor; + } + } + + private bool IsUdpDirectConnectionAvailable(Guid clientId) + { + return punchedEndpoints.ContainsKey(clientId); + } + + private bool TryGetTraversedEndpoint(Guid target, out EndpointGroup ep) + { + return punchedEndpoints.TryGetValue(target, out ep); + } + + private bool AddPunchedEndpoint(Guid clientId,EndpointGroup endpoints) + { + return punchedEndpoints.TryAdd(clientId, endpoints); + } + + private bool RemovePunchedEndpoint(Guid clientId, out EndpointGroup endpoints) + { + return punchedEndpoints.TryRemove(clientId, out endpoints); + } + + private void ClearPunchedEndpoints() + { + punchedEndpoints.Clear(); + } + + private bool TryGetAesTcpModule(Guid clientId, out AesTcpModule module) + { + return punchedTcpModules.TryGetValue(clientId, out module); + } + + private bool AddPunchedTcpModule(Guid clientId, AesTcpModule tcpModule) + { + return punchedTcpModules.TryAdd(clientId, tcpModule); + } + + private bool RemovePunchedTcpModule(Guid clientId, out AesTcpModule tcpModule) + { + return punchedTcpModules.TryRemove(clientId, out tcpModule); + } + + private void ClearPunchedTcpModules() + { + punchedTcpModules.Clear(); + } + + private bool TryGetRudpModules(Guid clientId, out List mod) + { + return RUdpModules.TryGetValue(clientId, out mod); + } + + private bool TryAddRUdpModules(Guid clientId, List mod) + { + return RUdpModules.TryAdd(clientId, mod); + } + + private void TryGetJumboUdpModule() + { + + } + #endregion + + #region Send + + private void SendUdpMesssageInternal(Guid toId, MessageEnvelope message) + { + GetUdpSendParams(toId, out ConcurrentAesAlgorithm algo, out IPEndPoint endpoint); if (message.PayloadCount > maxUdpPackageSize) { SendLargeUdpMessage(toId,message); return; } - - if (!udpServer.TrySendAsync(endpoint, message, algo, out var excessStream)) + try { + if (!udpServer.TrySendAsync(endpoint, message, algo, out var excessStream)) + { - if (JumboUdpModules.TryGetValue(toId, out var mod)) - mod.Send(excessStream.GetBuffer(), 0, excessStream.Position32); - else - MiniLogger.Log(MiniLogger.LogLevel.Error, "Unable To find jumbo module with Id: " + toId + " in session " + sessionId); + if (JumboUdpModules.TryGetValue(toId, out var mod)) + mod.Send(excessStream.GetBuffer(), 0, excessStream.Position32); + else + MiniLogger.Log(MiniLogger.LogLevel.Error, "Unable To find jumbo module with Id: " + toId + " in session " + sessionId); + } + } + catch (Exception ex) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "Unable To send udp message: " + ex.StackTrace); + Disconnect(); } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe void SendLargeUdpMessage(Guid toId, MessageEnvelope message) { - // you cat store buffer outside the scope.. + // you cant store buffer outside the scope.. if (message.KeyValuePairs != null) { var buffer = stackalloc byte[65000]; @@ -536,19 +849,7 @@ private unsafe void SendLargeUdpMessage(Guid toId, MessageEnvelope message) private void SendUdpMesssageInternal(Guid toId, MessageEnvelope message, T innerMessage) { - ConcurrentAesAlgorithm algo; - IPEndPoint endpoint; - if (punchedEndpoints.TryGetValue(toId, out var endpoints)) - { - endpoint = endpoints.ToSend; - algo = peerCryptos[endpoint].algorithm; - } - else - { - endpoint = relayServerEndpoint; - algo = udpEncryiptor; - - } + GetUdpSendParams(toId, out ConcurrentAesAlgorithm algo, out IPEndPoint endpoint); if (!udpServer.TrySendAsync(endpoint, message, innerMessage, algo, out var excessStream)) { @@ -563,21 +864,10 @@ private void SendUdpMesssageInternal(Guid toId, MessageEnvelope message, T in private void SendUdpMesssageInternal(Guid toId, MessageEnvelope message, Action serializationCallback) { - ConcurrentAesAlgorithm algo; - IPEndPoint endpoint; - if (punchedEndpoints.TryGetValue(toId, out var endpoints)) - { - endpoint = endpoints.ToSend; - algo = peerCryptos[endpoint].algorithm; - } - else - { - endpoint = relayServerEndpoint; - algo = udpEncryiptor; - - } + + GetUdpSendParams(toId, out ConcurrentAesAlgorithm algo, out IPEndPoint endpoint); - if (!udpServer.TrySendAsync(endpoint, message, serializationCallback, algo, out var excessStream,false)) + if (!udpServer.TrySendAsync(endpoint, message, serializationCallback, algo, out var excessStream, false)) { if (JumboUdpModules.TryGetValue(toId, out var mod)) mod.Send(excessStream.GetBuffer(), 0, excessStream.Position32); @@ -588,7 +878,6 @@ private void SendUdpMesssageInternal(Guid toId, MessageEnvelope message, Action< } - /// /// Sends the UDP message with bytes provided in envelope payload. /// @@ -643,7 +932,10 @@ public void SendUdpMessage(Guid toId, MessageEnvelope message, T innerMessage } - #region Broadcast/Multicast Udp + #region Broadcast/Multicast Udp + + + /// /// Broadcasts the UDP message to all connected peers. /// @@ -656,7 +948,7 @@ public void BroadcastUdpMessage(MessageEnvelope message) bool sendToRelay = false; foreach (var item in Peers) { - if (punchedEndpoints.ContainsKey(item.Key)) + if (IsUdpDirectConnectionAvailable(item.Key)) { message.To = item.Key; SendUdpMesssageInternal(item.Key, message); @@ -675,7 +967,7 @@ public void BroadcastUdpMessage(MessageEnvelope message) // unicast, message is too large. foreach (var item in Peers) { - if (!punchedEndpoints.ContainsKey(item.Key)) + if (!IsUdpDirectConnectionAvailable(item.Key)) { message.To = item.Key; SendUdpMesssageInternal(item.Key, message); @@ -701,7 +993,7 @@ public void BroadcastUdpMessage(MessageEnvelope message, T innerMessage) bool sendToRelay = false; foreach (var item in Peers) { - if (punchedEndpoints.ContainsKey(item.Key)) + if (IsUdpDirectConnectionAvailable(item.Key)) { message.To = item.Key; SendUdpMesssageInternal(item.Key, message, innerMessage); @@ -720,7 +1012,7 @@ public void BroadcastUdpMessage(MessageEnvelope message, T innerMessage) // unicast, message is too large. foreach (var item in Peers) { - if (!punchedEndpoints.ContainsKey(item.Key)) + if (!IsUdpDirectConnectionAvailable(item.Key)) { message.To = item.Key; SendUdpMesssageInternal(item.Key, message, innerMessage); @@ -733,13 +1025,14 @@ public void BroadcastUdpMessage(MessageEnvelope message, T innerMessage) } internal void MulticastUdpMessage(MessageEnvelope message, ICollection targets) { + if (Peers.Count < 0) return; message.From = sessionId; bool sendToRelay = false; foreach (var target in targets) { - if (punchedEndpoints.TryGetValue(target, out var ep)) + if (TryGetTraversedEndpoint(target, out var ep)) { message.To = target; SendUdpMesssageInternal(target, message); @@ -759,7 +1052,7 @@ internal void MulticastUdpMessage(MessageEnvelope message, ICollection tar { if (target == sessionId) continue; - if (!punchedEndpoints.ContainsKey(target)) + if (!IsUdpDirectConnectionAvailable(target)) { message.To = target; SendUdpMesssageInternal(target, message); @@ -778,7 +1071,7 @@ internal void MulticastUdpMessage(MessageEnvelope message, ICollection bool sendToRelay = false; foreach (var target in targets) { - if (punchedEndpoints.TryGetValue(target, out var ep)) + if (TryGetTraversedEndpoint(target, out var ep)) { message.To = target; SendUdpMesssageInternal(target, message,innerMessage); @@ -795,7 +1088,7 @@ internal void MulticastUdpMessage(MessageEnvelope message, ICollection { foreach (var target in targets) { - if (!punchedEndpoints.ContainsKey(target)) + if (!IsUdpDirectConnectionAvailable(target)) { if (target == sessionId) continue; @@ -804,11 +1097,10 @@ internal void MulticastUdpMessage(MessageEnvelope message, ICollection SendUdpMesssageInternal(target, message,innerMessage); } } - } } - } + #endregion /// @@ -823,7 +1115,7 @@ public void BroadcastMessage(MessageEnvelope message) //message.To = Guid.Empty; foreach (var peer in Peers) { - if (punchedTcpModules.TryGetValue(peer.Key, out var module)) + if (TryGetAesTcpModule(peer.Key, out var module)) { // message.To = peer.Key; module.SendAsync(message); @@ -866,7 +1158,7 @@ public void SendAsyncMessage(Guid toId, MessageEnvelope message) message.From = sessionId; message.To = toId; - if(punchedTcpModules.TryGetValue(toId, out var module)) + if(TryGetAesTcpModule(toId, out var module)) { module.SendAsync(message); } @@ -888,7 +1180,7 @@ public void SendAsyncMessage(Guid toId, MessageEnvelope envelope, T message) envelope.From = sessionId; envelope.To = toId; - if (punchedTcpModules.TryGetValue(toId, out var module)) + if (TryGetAesTcpModule(toId, out var module)) { module.SendAsync(envelope, message); } @@ -909,7 +1201,7 @@ public void SendAsyncMessage(Guid toId, MessageEnvelope envelope, Action(Guid toId, T message, string messageHeader = nul }; envelope.Header = messageHeader == null ? typeof(T).Name : messageHeader; - if (punchedTcpModules.TryGetValue(toId, out var module)) + if (TryGetAesTcpModule(toId, out var module)) { module.SendAsync(envelope,message); } @@ -963,13 +1255,14 @@ public Task SendRequestAndWaitResponse(Guid toId, T message, MessageId = Guid.NewGuid(), Header = messageHeader == null ? typeof(T).Name : messageHeader }; + Task task; - if (punchedTcpModules.TryGetValue(toId, out var module)) + if (TryGetAesTcpModule(toId, out var module)) { task = module.SendMessageAndWaitResponse(envelope, message,timeoutMs); } else - task = tcpMessageClient.SendMessageAndWaitResponse(envelope, message, timeoutMs); + task = tcpMessageClient.SendMessageAndWaitResponse(envelope, message, timeoutMs); return task; } @@ -987,7 +1280,7 @@ public Task SendRequestAndWaitResponse(Guid toId, MessageEnvelo message.To = toId; Task task; - if (punchedTcpModules.TryGetValue(toId, out var module)) + if (TryGetAesTcpModule(toId, out var module)) { task = module.SendMessageAndWaitResponse(message, timeoutMs); } @@ -1011,7 +1304,7 @@ public Task SendRequestAndWaitResponse(Guid toId, MessageEnv envelope.From = sessionId; envelope.To = toId; Task task; - if (punchedTcpModules.TryGetValue(toId, out var module)) + if (TryGetAesTcpModule(toId, out var module)) { task = module.SendMessageAndWaitResponse(envelope, message, timeoutMs); } @@ -1040,7 +1333,6 @@ private void HandleUdpBytesReceived(IPEndPoint adress, byte[] bytes, int offset, if (peerCryptos.TryGetValue(adress, out var crypto)) { byte[] decryptBuffer = null; - try { MessageEnvelope msg; @@ -1061,7 +1353,16 @@ void ParseMessage(byte[] decryptedBytes, int byteOffset, int byteCount) if(msg.To == Guid.Empty) msg.To = sessionId; if(adress != relayServerEndpoint && msg.From == Guid.Empty) - msg.From = peerCryptos[adress].id; + { + if (crypto == null) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "Unable to find decrptor from " + adress); + return; + } + + msg.From = crypto.id; + } + if (!clientStateManager.HandleMessage(adress, msg)) { HandleUdpMessageReceived(msg); @@ -1073,7 +1374,7 @@ void ParseMessage(byte[] decryptedBytes, int byteOffset, int byteCount) catch (Exception e) { var b = decryptBuffer; - MiniLogger.Log(MiniLogger.LogLevel.Error, "Relay Client Failed to deserialise envelope message " + e.Message); + MiniLogger.Log(MiniLogger.LogLevel.Error, "Relay Client Failed to deserialise envelope message " + e.Message +"\n"+ e.StackTrace); } } @@ -1102,23 +1403,26 @@ private void HandleUdpMessageReceived(MessageEnvelope message) break; // reliable udp case Constants.Rudp: - if (RUdpModules.TryGetValue(message.From, out var mod)) + if (TryGetRudpModules(message.From, out var mod)) { mod[0].HandleBytes(message.Payload, message.PayloadOffset, message.PayloadCount); } break; case Constants.Rudp1: - if (RUdpModules.TryGetValue(message.From, out var mod2)) + if (TryGetRudpModules(message.From, out var mod2)) { mod2[1].HandleBytes(message.Payload, message.PayloadOffset, message.PayloadCount); } break; case Constants.Rudp2: - if (RUdpModules.TryGetValue(message.From, out var mod3)) + if (TryGetRudpModules(message.From, out var mod3)) { mod3[2].HandleBytes(message.Payload, message.PayloadOffset, message.PayloadCount); } break; + case Constants.KeepAlieve: + tcpMessageClient.SendAsyncMessage(message); + break; default: HandleUdpMessage(message); break; @@ -1142,6 +1446,9 @@ private void HandleMessageReceived(MessageEnvelope message) UpdatePeerList(message); else if (message.Header == "CmdTcpHp") InitiateRemoteTcpHolepunch(message); + else if(message.Header == Constants.KeepAlieve) + tcpMessageClient.SendAsyncMessage(message); + else HandleMessage(message); } @@ -1161,7 +1468,7 @@ private void HandleMessageReceived(MessageEnvelope message) case Constants.Pong: HandlePong(message); break; - + default: HandleMessage(message); break; @@ -1207,7 +1514,7 @@ public Task RequestHolePunchAsync(Guid peerId, int timeOut, bool encrypted { return Task.FromResult(false); } - if (punchedEndpoints.ContainsKey(peerId)) + if (IsUdpDirectConnectionAvailable(peerId)) { return Task.FromResult(true); } @@ -1322,8 +1629,8 @@ internal void HandleHolepunchSuccess(ClientHolepunchState state) algorithm = new ConcurrentAesAlgorithm(state.cryptoKey, AESMode) }); } - - punchedEndpoints.TryAdd(state.destinationId, new EndpointGroup() { ToReceive = state.succesfulEpToReceive,ToSend = state.succesfullEpToSend }); + + AddPunchedEndpoint(state.destinationId, new EndpointGroup() { ToReceive = state.succesfulEpToReceive,ToSend = state.succesfullEpToSend }); MiniLogger.Log(MiniLogger.LogLevel.Info, $"HolePunched, Receive Endpoint: {state.succesfulEpToReceive}, Send Endpoint {state.succesfullEpToSend}"); } @@ -1432,13 +1739,13 @@ protected internal void HandleUnRegistered(Guid peerId) { Peers.TryRemove(peerId, out _); - if (punchedEndpoints.TryRemove(peerId, out var ep) && ep != null) + if (RemovePunchedEndpoint(peerId, out var ep) && ep != null) { peerCryptos.TryRemove(ep.ToReceive, out _); peerCryptos.TryRemove(ep.ToSend, out _); } - if(punchedTcpModules.TryRemove(peerId,out var module)) + if(RemovePunchedTcpModule(peerId,out var module)) { module.Dispose(); } @@ -1503,7 +1810,7 @@ private void SendUdpRaw(Guid toId, MessageEnvelope message, byte[] b, int o, int if (!Peers.TryGetValue(toId, out _) && toId != sessionId) return; int offset = 0; - if (punchedEndpoints.ContainsKey(toId)) + if (IsUdpDirectConnectionAvailable(toId)) { message.To = Guid.Empty; message.From = Guid.Empty; @@ -1526,7 +1833,7 @@ private void SendUdpRaw(Guid toId, MessageEnvelope message, byte[] b, int o, int ConcurrentAesAlgorithm algo; IPEndPoint endpoint; - if (punchedEndpoints.TryGetValue(toId, out var endpoints)) + if (TryGetTraversedEndpoint(toId, out var endpoints)) { endpoint = endpoints.ToSend; algo = peerCryptos[endpoint].algorithm; @@ -1596,7 +1903,7 @@ private void CreateRudpModule(Guid peer) mod1, mod2 }; - RUdpModules.TryAdd(peer, l); + TryAddRUdpModules(peer, l); } @@ -1643,7 +1950,7 @@ private void FrameRudpMessage(Guid toId,int ch, byte[] b, int o, int c) /// The channel. public void SendRudpMessage(Guid to, MessageEnvelope msg, RudpChannel channel = RudpChannel.Ch1) { - if (RUdpModules.TryGetValue(to, out var mod)) + if (TryGetRudpModules(to, out var mod)) { //WriteRudpRouterHeaderIfNeeded(msg, to); msg = MessageEnvelope.CloneWithNoRouter(msg); @@ -1675,7 +1982,7 @@ public void SendRudpMessage(Guid to, MessageEnvelope msg, RudpChannel channel = /// The channel. public void SendRudpMessage(Guid to, MessageEnvelope msg, T innerMessage, RudpChannel channel = RudpChannel.Ch1) { - if (RUdpModules.TryGetValue(to, out var mod)) + if (TryGetRudpModules(to, out var mod)) { //WriteRudpRouterHeaderIfNeeded(msg, to); msg = MessageEnvelope.CloneWithNoRouter(msg); @@ -1699,7 +2006,7 @@ public void SendRudpMessage(Guid to, MessageEnvelope msg, T innerMessage, Rud /// public Task SendRudpMessageAndWaitResponse(Guid to, MessageEnvelope msg, int timeoutMs = 10000, RudpChannel channel = RudpChannel.Ch1) { - if (RUdpModules.TryGetValue(to, out var mod)) + if (TryGetRudpModules(to, out var mod)) { //WriteRudpRouterHeaderIfNeeded(msg, to); msg = MessageEnvelope.CloneWithNoRouter(msg); @@ -1739,7 +2046,7 @@ public Task SendRudpMessageAndWaitResponse(Guid to, MessageEnve /// public Task SendRudpMessageAndWaitResponse(Guid to, MessageEnvelope msg, T innerMessage, int timeoutMs = 10000, RudpChannel channel = RudpChannel.Ch1) { - if (RUdpModules.TryGetValue(to, out var mod)) + if (TryGetRudpModules(to, out var mod)) { //WriteRudpRouterHeaderIfNeeded(msg, to); msg = MessageEnvelope.CloneWithNoRouter(msg); @@ -1765,7 +2072,7 @@ private void WriteRudpRouterHeaderIfNeeded(MessageEnvelope msg,Guid to) msg.To = Guid.Empty; msg.From = Guid.Empty; return; - if (punchedEndpoints.ContainsKey(to)) + if (IsUdpDirectConnectionAvailable(to)) { msg.To = Guid.Empty; msg.From = Guid.Empty; @@ -1840,7 +2147,7 @@ public void Dispose() { module.Value.Dispose(); } - punchedTcpModules.Clear(); + ClearPunchedTcpModules(); } } @@ -1860,7 +2167,7 @@ internal void RegisterTcpNode(IState istate) } module.OnMessageReceived += HandleMessageReceived; - punchedTcpModules.TryAdd(state.destinationId, module); + AddPunchedTcpModule(state.destinationId, module); Console.WriteLine("REG TCP HP!!"); } diff --git a/NetworkLibrary/P2P/Generic/RelayServerBase.cs b/NetworkLibrary/P2P/Generic/RelayServerBase.cs index 529c049..264e32a 100644 --- a/NetworkLibrary/P2P/Generic/RelayServerBase.cs +++ b/NetworkLibrary/P2P/Generic/RelayServerBase.cs @@ -1,4 +1,5 @@ -using NetworkLibrary.Components; +using MessageProtocol; +using NetworkLibrary.Components; using NetworkLibrary.Components.Crypto; using NetworkLibrary.Components.Statistics; using NetworkLibrary.MessageProtocol; @@ -11,6 +12,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; @@ -27,10 +30,11 @@ namespace NetworkLibrary.P2P.Generic private AsyncUdpServer udpServer; internal GenericMessageSerializer serialiser = new GenericMessageSerializer(); - private ConcurrentDictionary RegisteredPeers = new ConcurrentDictionary(); + protected ConcurrentDictionary RegisteredPeers = new ConcurrentDictionary(); private ConcurrentDictionary RegisteredUdpEndpoints = new ConcurrentDictionary(); private ConcurrentDictionary> ClientUdpEndpoints = new ConcurrentDictionary>(); private ConcurrentDictionary UdpCryptos = new ConcurrentDictionary(); + private ConcurrentDictionary endpointMap = new ConcurrentDictionary(); internal ConcurrentDictionary> peerReachabilityMatrix = new ConcurrentDictionary>(); private TaskCompletionSource PushPeerList = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -42,6 +46,7 @@ internal ConcurrentDictionary> peerReac public AesMode AESMode; bool initialised = false; int port = 0; + Stopwatch serverClock = new Stopwatch(); public SecureRelayServerBase(int port, X509Certificate2 certificate, string serverName = "Andromeda") : base(port, certificate) { this.port = port; @@ -64,9 +69,15 @@ public SecureRelayServerBase(int port, string serverName = "Andromeda") : base(p } public override void StartServer() { + serverClock.Start(); Initialise(); base.StartServer(); } + + public virtual double GetTime() + { + return serverClock.Elapsed.TotalMilliseconds; + } private void Initialise() { if(initialised) return; @@ -86,6 +97,48 @@ private void Initialise() udpServer.StartServer(); Task.Run(PeerListPushRoutine); + Task.Run(VerifyHangedClients); + } + public void FlushAllClients() + { + foreach (var session in Sessions.ToList()) + { + session.Value.EndSession(); + } + } + private async void VerifyHangedClients() + { + MessageEnvelope envelope = new MessageEnvelope(); + envelope.Header = Constants.KeepAlieve; + envelope.IsInternal = true; + while (!shutdown) + { + await Task.Delay(10000); + foreach (var session in Sessions.ToList()) + { + bool badSession = await VerifyTcp(envelope, session); + if (badSession) + { + session.Value.EndSession(); + } + } + } + + async Task VerifyTcp(MessageEnvelope envelope_, KeyValuePair session) + { + bool badSession = true; + for (var i = 0; i < 2; i++) + { + var response = await SendMessageAndWaitResponse(session.Key, envelope_, timeoutMs: 5000); + if (response != null && response.Header != MessageEnvelope.RequestTimeout) + { + badSession = false; + break; + } + } + + return badSession; + } } public void GetTcpStatistics(out TcpStatistics generalStats, out ConcurrentDictionary sessionStats) @@ -141,7 +194,9 @@ protected virtual void NotifyCurrentPeerList(Guid clientId) peerList[peer.Key] = new PeerInfo() { Address = GetIPEndPoint(peer.Key).Address.GetAddressBytes(), - Port = (ushort)GetIPEndPoint(peer.Key).Port + Port = (ushort)GetIPEndPoint(peer.Key).Port, + RegisteryTime = peer.Value + }; } } @@ -166,10 +221,10 @@ internal void Register(Guid clientId, IPEndPoint remoteEndpoint, List()); @@ -410,6 +475,9 @@ private void HandleUdpBytesReceived(IPEndPoint adress, byte[] bytes, int offset, catch (Exception e) { MiniLogger.Log(MiniLogger.LogLevel.Error, "Udp Relay failed to deserialise envelope message " + e.Message); + //kill client here. + if (endpointMap.TryGetValue(adress, out var id)) + CloseSession(id); return; } finally @@ -454,16 +522,17 @@ protected virtual void BroadcastUdp(byte[] buffer, int decrptedAmount) udpServer.SendBytesToClient(destEp, tempBuff, 0, reEncryptedBytesAmount); } } - } private void HandleUnregistreredMessage(IPEndPoint adress, byte[] bytes, int offset, int count) { + // local discovery udp bc if(count == 1 && bytes[offset] == 91) { udpServer.SendBytesToClient(adress, serverNameBytes,0,serverNameBytes.Length); return; } + //EndpointTransferMessage if (bytes[offset] == 92 && bytes[offset+1] == 93) { try diff --git a/NetworkLibrary/P2P/Generic/Room/SecureLobbyClient.cs b/NetworkLibrary/P2P/Generic/Room/SecureLobbyClient.cs index 3ba110e..2566ca5 100644 --- a/NetworkLibrary/P2P/Generic/Room/SecureLobbyClient.cs +++ b/NetworkLibrary/P2P/Generic/Room/SecureLobbyClient.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net.Security; using System.Security.Cryptography.X509Certificates; @@ -18,13 +19,14 @@ namespace NetworkLibrary.P2P.Generic.Room public Guid SessionId => client.SessionId; public bool IsConnected => client.IsConnected; - public Action OnPeerJoinedRoom; - public Action OnPeerLeftRoom; + public Action OnPeerJoinedRoom; + public Action OnPeerLeftRoom; public Action OnPeerDisconnected; //public Action OnTcpRoomMesssageReceived; //public Action OnUdpRoomMesssageReceived; public Action OnTcpMessageReceived; public Action OnUdpMessageReceived; + public Action> AvailableRoomsChanged; public Action OnDisconnected; public RemoteCertificateValidationCallback RemoteCertificateValidationCallback; private RelayClientBase client; @@ -35,7 +37,6 @@ private ConcurrentDictionary // [peerId] => Collection private ConcurrentDictionary> peersInRooms = new ConcurrentDictionary>(); - public SecureLobbyClient(X509Certificate2 clientCert) { client = new RelayClientBase(clientCert); @@ -43,9 +44,15 @@ public SecureLobbyClient(X509Certificate2 clientCert) client.OnUdpMessageReceived += HandleUdpMessage; client.OnDisconnected += HandleDisconnected; client.RemoteCertificateValidationCallback += CertificateValidation; - + } + public Task SyncTime() => client.SyncTime(); + public void StartAutoTimeSync(int periodMS, bool disconnectOnTimeout, bool usePTP = false) => client.StartAutoTimeSync(periodMS,disconnectOnTimeout, usePTP); + + public void StopAutoTimeSync()=>client.StopAutoTimeSync(); + public double GetTime() => client.GetTime(); + public DateTime GetDateTime() => client.GetDateTime(); private bool CertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (RemoteCertificateValidationCallback == null) @@ -78,14 +85,15 @@ public Task RequestTcpHolePunchAsync(Guid destinationId, int timeot = 1000 return client.RequestTcpHolePunchAsync(destinationId); } - public void CreateOrJoinRoom(string roomName) + public RoomPeerList CreateOrJoinRoom(string roomName) { - _ = CreateOrJoinRoomAsync(roomName).Result; + return CreateOrJoinRoomAsync(roomName).Result; } - public Task CreateOrJoinRoomAsync(string roomName) + + public Task CreateOrJoinRoomAsync(string roomName) { - var returnVal = new TaskCompletionSource(); + var returnVal = new TaskCompletionSource(); var message = new MessageEnvelope(); message.IsInternal = true; @@ -102,12 +110,13 @@ public Task CreateOrJoinRoomAsync(string roomName) { if (response.Result.Header != MessageEnvelope.RequestTimeout) { - returnVal.TrySetResult(true); + var currentList = KnownTypeSerializer.DeserializeRoomPeerList(response.Result.Payload, response.Result.PayloadOffset); + returnVal.TrySetResult(currentList); } else { rooms.TryRemove(roomName, out _); - returnVal.TrySetResult(false); + returnVal.TrySetResult(null); } }); return returnVal.Task; @@ -167,9 +176,19 @@ private void PrepareEnvelopeBC(string roomName, ref MessageEnvelope messageEnvel if (messageEnvelope.KeyValuePairs == null) messageEnvelope.KeyValuePairs = new Dictionary(); - messageEnvelope.KeyValuePairs[Constants.RoomName] = roomName; + messageEnvelope.KeyValuePairs[Constants.RoomBroadcast] = roomName; messageEnvelope.To = Guid.Empty; - messageEnvelope.From = client.SessionId; + messageEnvelope.From = Guid.Empty; + } + + private void PrepareEnvelopeBCBatched(string roomName, ref MessageEnvelope messageEnvelope) + { + if (messageEnvelope.KeyValuePairs == null) + messageEnvelope.KeyValuePairs = new Dictionary(); + + messageEnvelope.KeyValuePairs[Constants.RoomBroadcastBatched] = roomName; + messageEnvelope.To = Guid.Empty; + messageEnvelope.From = Guid.Empty; } private void PrepareEnvelopeDM(string roomName, ref MessageEnvelope messageEnvelope) @@ -177,7 +196,7 @@ private void PrepareEnvelopeDM(string roomName, ref MessageEnvelope messageEnvel if (messageEnvelope.KeyValuePairs == null) messageEnvelope.KeyValuePairs = new Dictionary(); - messageEnvelope.KeyValuePairs[Constants.RoomName] = roomName; + messageEnvelope.KeyValuePairs[Constants.RoomBroadcast] = roomName; messageEnvelope.From = client.SessionId; } @@ -252,6 +271,27 @@ public void BroadcastRudpMessageToRoom(string roomName, MessageEnvelope messa } } } + + //--- + public void BroadcastMessageToRoomBatched(string roomName, MessageEnvelope message) + { + if (CanSend(roomName)) + { + PrepareEnvelopeBCBatched(roomName, ref message); + client.tcpMessageClient.SendAsyncMessage(message); + } + } + + public void BroadcastMessageToRoomBatched(string roomName, MessageEnvelope message, T innerMessage) + { + if (CanSend(roomName)) + { + PrepareEnvelopeBCBatched(roomName, ref message); + client.tcpMessageClient.SendAsyncMessage(message, innerMessage); + } + } + + //--- #endregion #region Direct Messages @@ -334,6 +374,10 @@ private void HandleMessage(MessageEnvelope message) { UpdateRooms(message); } + else if(message.Header == Constants.RoomAvalibalilityUpdate) + { + AvailableRoomsChanged?.Invoke(message.KeyValuePairs?.Keys?.ToList()?? new List()); + } else if (message.Header == Constants.PeerDisconnected) { HandlePeerDisconnected(message); @@ -359,13 +403,16 @@ private void UpdateRooms(MessageEnvelope message) var roomUpdateMessage = KnownTypeSerializer.DeserializeRoomPeerList(message.Payload, message.PayloadOffset); Dictionary JoinedList = new Dictionary(); - List LeftList = new List(); + Dictionary LeftList = new Dictionary(); var remoteList = roomUpdateMessage.Peers.PeerIds; if (!rooms.TryGetValue(roomUpdateMessage.RoomName, out var localRoom)) { return; } + + if(remoteList.ContainsKey(SessionId)) + remoteList.Remove(SessionId); foreach (var remotePeer in remoteList) { @@ -380,7 +427,8 @@ private void UpdateRooms(MessageEnvelope message) { if (!remoteList.ContainsKey(localPeerId)) { - LeftList.Add(localPeerId); + localRoom.TryGetPeerInfo(localPeerId, out PeerInfo peerInfo);// cant fail + LeftList[localPeerId] = peerInfo; } } @@ -399,28 +447,28 @@ private void UpdateRooms(MessageEnvelope message) peersInRooms[peerKV.Key].TryAdd(roomUpdateMessage.RoomName, null); client.HandleRegistered(peerKV.Key, roomUpdateMessage.Peers.PeerIds); - OnPeerJoinedRoom?.Invoke(roomUpdateMessage.RoomName, peerKV.Key); + OnPeerJoinedRoom?.Invoke(roomUpdateMessage.RoomName, peerKV.Key, peerKV.Value); } foreach (var peerId in LeftList) { if (rooms.TryGetValue(roomUpdateMessage.RoomName, out var room)) { - room.Remove(peerId); + room.Remove(peerId.Key); } - if (peersInRooms.TryGetValue(peerId, out var roomList)) + if (peersInRooms.TryGetValue(peerId.Key, out var roomList)) { roomList.TryRemove(roomUpdateMessage.RoomName, out _); if(roomList.Count == 0) { - peersInRooms.TryRemove(peerId, out _); - HandlePeerDisconnected_(peerId); + peersInRooms.TryRemove(peerId.Key, out _); + HandlePeerDisconnected_(peerId.Key); } } //client.HandleUnRegistered(peerId); - OnPeerLeftRoom?.Invoke(roomUpdateMessage.RoomName, peerId); + OnPeerLeftRoom?.Invoke(roomUpdateMessage.RoomName, peerId.Key, peerId.Value); } } diff --git a/NetworkLibrary/P2P/Generic/Room/SecureLobbyServer.cs b/NetworkLibrary/P2P/Generic/Room/SecureLobbyServer.cs index fd1e6ba..720be21 100644 --- a/NetworkLibrary/P2P/Generic/Room/SecureLobbyServer.cs +++ b/NetworkLibrary/P2P/Generic/Room/SecureLobbyServer.cs @@ -3,10 +3,12 @@ using NetworkLibrary.P2P.Components; using NetworkLibrary.P2P.Components.HolePunch; using NetworkLibrary.P2P.Generic; +using NetworkLibrary.UDP.Jumbo; using NetworkLibrary.Utils; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading; @@ -22,14 +24,159 @@ private ConcurrentDictionary> private ConcurrentDictionary> peerToRoomMap = new ConcurrentDictionary>(); - // representspeers that have contacted. used for broadasting disconnect message + // represents peers that have contacted. used for broadasting disconnect message private ConcurrentDictionary> peerRelationalMap = new ConcurrentDictionary>(); private object roomLock = new object(); + public int dispatchms = 10; + + AsyncDispatcher batchBroadcastDispatcher = new AsyncDispatcher(); + AsyncDispatcher roomPublishOperation = new AsyncDispatcher(); + ConcurrentDictionary> tcpBatchBrodacastCollector = new ConcurrentDictionary>(); + public int NumClients=> RegisteredPeers.Count; public SecureLobbyServer(int port, X509Certificate2 cerificate) : base(port, cerificate) { + + } + + public override void StartServer() + { + StartRoomPublisher(); + Task.Run(() => { StartPeriodicMessageDispatcher(); }); + base.StartServer(); + } + private async void StartRoomPublisher() + { + try + { + await roomPublishOperation.ExecuteBySignal(() => + { + + MessageEnvelope roomList = CreateRoomListMsg(); + + foreach (var rp in RegisteredPeers) + { + SendAsyncMessage(rp.Key, roomList); + } + + + }).ConfigureAwait(false); + } + catch (AggregateException e) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, + $"An Error occured on Room publisher: \n{e.InnerException.Message}\n{e.InnerException.StackTrace}"); + + StartRoomPublisher(); + } + + } + + private async void StartPeriodicMessageDispatcher() + { + Dictionary> toDispatch = new Dictionary>(); + + try + { + await batchBroadcastDispatcher.LoopPeriodic(() => + { + + foreach (var roomBCMsgs in tcpBatchBrodacastCollector) + { + + string roomName = roomBCMsgs.Key; + ICollection roomPeerIds; + + if (TryGetRoom(roomName, out Room room)) + { + roomPeerIds = room.GetPeerIdList(); + } + else + { + if (tcpBatchBrodacastCollector.TryRemove(roomBCMsgs.Key, out ConcurrentQueue queue)) + { + while (queue.TryDequeue(out BroadcastBytes msg)) + { + msg.ReturnBuffer(); + } + } + + continue; + } + + List disposalList = new List(roomBCMsgs.Value.Count+10); + while (roomBCMsgs.Value.TryDequeue(out BroadcastBytes msgToBc)) + { + Guid senderId = msgToBc.From; + disposalList.Add(msgToBc); + + foreach (var destination in roomPeerIds) + { + if (destination != senderId) + { + if (!toDispatch.ContainsKey(destination)) + { + toDispatch[destination] = new Queue(); + } + toDispatch[destination].Enqueue(msgToBc);// has duplicates.. + } + } + } + + + Parallel.ForEach(toDispatch, toDispatchKv => + { + var segments = new List>(); + + while (toDispatchKv.Value.Count > 0) + { + BroadcastBytes msg = toDispatchKv.Value.Dequeue(); + segments.Add(new ArraySegment(msg.Data, msg.Offset, msg.Count)); + + } + + if (segments.Count > 0) + SendSegmentsToClient(toDispatchKv.Key, segments); + + }); + + foreach (var container in disposalList) + { + container.ReturnBuffer(); + } + disposalList.Clear(); + + } + + }, dispatchms).ConfigureAwait(false); + + } + catch (AggregateException e) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, + $"An Error occured on Batch broadcast dispatch: \n{e.InnerException.Message}\n{e.InnerException.StackTrace}"); + + StartPeriodicMessageDispatcher(); + } + } + + private MessageEnvelope CreateRoomListMsg() + { + MessageEnvelope roomList = new MessageEnvelope(); + roomList.Header = Constants.RoomAvalibalilityUpdate; + roomList.KeyValuePairs = new Dictionary(); + lock (roomLock) + { + foreach (var item in rooms) + { + roomList.KeyValuePairs.Add(item.Key, null); + } + return roomList; + } + } + #region Overrides #region Override Disbale Behaviour protected override void NotifyCurrentPeerList(Guid clientId) @@ -40,12 +187,15 @@ protected override void NotifyCurrentPeerList(Guid clientId) protected override void PublishPeerRegistered(Guid clientId) { // this on base sets the task to publish peer list - // we dont care + // only send available room to newcomer + var roomList = CreateRoomListMsg(); + SendAsyncMessage(clientId, roomList); } #endregion protected override void PublishPeerUnregistered(Guid clientId) { + // here we care because it could be dc. //TODO Send a Special Disconnect Message.... to who ?? HashSet notificationList = new HashSet(); @@ -58,14 +208,21 @@ protected override void PublishPeerUnregistered(Guid clientId) foreach (var item in roomList) { var roomName = item.Key; - if (rooms.TryGetValue(roomName, out var room)) + if (TryGetRoom(roomName, out var room)) { - room.Remove(clientId); + int remaining = room.Remove(clientId); var peerList = room.GetPeerIdList(); foreach (var id in peerList) { notificationList.Add(id); } + if (remaining == 0) + { + TryRemoveRoom(roomName, out _); + MiniLogger.Log(MiniLogger.LogLevel.Info, $"[{roomName}] Room Destroyed"); + room.Close(); + } + } } } @@ -104,39 +261,55 @@ protected override void HandleMessageReceivedInternal(Guid clientId, MessageEnve private void SendAvailableRooms(Guid clientId, MessageEnvelope message) { - message.KeyValuePairs = new Dictionary(); - foreach (var item in rooms) + lock (roomLock) { - message.KeyValuePairs.Add(item.Key, null); + message.KeyValuePairs = new Dictionary(); + foreach (var item in rooms) + { + message.KeyValuePairs.Add(item.Key, null); + } } + SendAsyncMessage(clientId, message); } #endregion - private void CreateOrJoinRoom(Guid clientID, MessageEnvelope message) + private void CreateOrJoinRoom(Guid clientId, MessageEnvelope message) { if (message.KeyValuePairs != null) { var roomName = message.KeyValuePairs.Keys.First(); Room room = null; + MessageEnvelope peerListMsg = new MessageEnvelope(); lock (roomLock) { - if (!rooms.TryGetValue(roomName, out room)) + if (!TryGetRoom(roomName, out room)) { room = new Room(roomName, this); - rooms.TryAdd(roomName, room); - //Console.WriteLine(roomName + " Added"); + TryAddRoom(roomName, room); MiniLogger.Log(MiniLogger.LogLevel.Info, $"Room Created [{roomName}]"); } - room.Add(clientID); - // Console.WriteLine(roomName + " Joined"); - MiniLogger.Log(MiniLogger.LogLevel.Info, $"{clientID} Joined the room [{roomName}]"); + try + { + room.LockRoom(); + room.Add(clientId); + + if (!peerToRoomMap.ContainsKey(clientId)) + peerToRoomMap.TryAdd(clientId, new ConcurrentDictionary()); + + peerToRoomMap[clientId].TryAdd(roomName, null); + + peerListMsg = room.GetPeerListMsg(); + peerListMsg.MessageId = message.MessageId; + SendAsyncMessage(clientId, peerListMsg); + + MiniLogger.Log(MiniLogger.LogLevel.Info, $"{clientId} Joined the room [{roomName}]"); + } + finally { room.UnlockRoom(); } + - if (!peerToRoomMap.ContainsKey(clientID)) - peerToRoomMap.TryAdd(clientID, new ConcurrentDictionary()); - peerToRoomMap[clientID].TryAdd(roomName, null); } - SendAsyncMessage(clientID, new MessageEnvelope() { MessageId = message.MessageId }); + // here send the room state, all the peers inside. } } @@ -147,92 +320,250 @@ private void LeaveRoom(Guid clientID, MessageEnvelope message) var roomName = message.KeyValuePairs.Keys.First(); lock (roomLock) { - if (rooms.TryGetValue(roomName, out var room)) + if (TryGetRoom(roomName, out var room)) { - room.Remove(clientID); + try + { + room.LockRoom(); + + int remaining = room.Remove(clientID); + if (remaining == 0) + { + TryRemoveRoom(roomName, out _); + MiniLogger.Log(MiniLogger.LogLevel.Info, $"[{roomName}] Room Destroyed"); + room.Close(); + } + + if (peerToRoomMap.ContainsKey(clientID)) + peerToRoomMap[clientID].TryRemove(roomName, out _); + } + finally + { + room.UnlockRoom(); + } + } - if (peerToRoomMap.ContainsKey(clientID)) - peerToRoomMap[clientID].TryRemove(roomName, out _); + + } + } + } + + class BroadcastBytes + { + public Guid From; + public int Offset; + public int Count; + public byte[] Data => GetData(); + private byte[] GetData() + { + if (Interlocked.CompareExchange(ref released, 0, 0) == 1) + { + throw new InvalidOperationException("Attempted to access released buffer"); + } + return data; + } + + private byte[] data; + private int released = 0; + private PooledMemoryStream stream; + + + public BroadcastBytes(MessageEnvelope msg, GenericMessageSerializer serializer) + { + this.From = msg.From; + + stream = SharerdMemoryStreamPool.RentStreamStatic(); + stream.Position = 0; + serializer.EnvelopeMessageWithBytes(stream, msg, msg.Payload, msg.PayloadOffset, msg.PayloadCount); + + var buff = stream.GetBuffer(); + this.data = buff; + this.Offset = 0; + this.Count = stream.Position32; + } + + internal void ReturnBuffer() + { + if(Interlocked.Exchange(ref released, 1) == 0) + { + var buff = Interlocked.Exchange(ref data, null); + SharerdMemoryStreamPool.ReturnStreamStatic(stream); } + } } - protected override void BroadcastMessage(Guid guid, byte[] bytes, int offset, int count) + + protected override void BroadcastMessage(Guid senderId, byte[] bytes, int offset, int count) { var messageEnvelope = serialiser.DeserialiseEnvelopedMessage(bytes, offset, count); - if (messageEnvelope.KeyValuePairs != null && messageEnvelope.KeyValuePairs.TryGetValue(Constants.RoomName, out string roomName)) + messageEnvelope.From = senderId; + + if (messageEnvelope.KeyValuePairs != null) { - rooms.TryGetValue(roomName, out var room); - var snapshot = room.GetPeerIdList(); - foreach (var peerId in snapshot) + + if (messageEnvelope.KeyValuePairs.TryGetValue(Constants.RoomBroadcast, out string roomName)) { - if (peerId != guid) - SendBytesToClient(peerId, bytes, offset, count); + TryGetRoom(roomName, out var room); + var snapshot = room.GetPeerIdList(); + + messageEnvelope.KeyValuePairs.Remove(Constants.RoomBroadcast); + if (messageEnvelope.KeyValuePairs.Count == 0) + messageEnvelope.KeyValuePairs = null; + + foreach (var peerId in snapshot) + { + if (peerId != senderId) + SendAsyncMessage(peerId, messageEnvelope); + } + } + else if (messageEnvelope.KeyValuePairs.TryGetValue(Constants.RoomBroadcastBatched, out roomName)) + { + messageEnvelope.KeyValuePairs.Remove(Constants.RoomBroadcastBatched); + if (messageEnvelope.KeyValuePairs.Count == 0) + messageEnvelope.KeyValuePairs = null; + + PushBatchQueueTcp(roomName, messageEnvelope); } } } + private void PushBatchQueueTcp(string roomName,MessageEnvelope msg) + { + var queue = tcpBatchBrodacastCollector.GetOrAdd(roomName, new ConcurrentQueue()); + queue.Enqueue(new BroadcastBytes(msg,serialiser)); + } + // we have to do something about this in future when we have reliable udp. protected override void BroadcastUdp(byte[] buffer, int lenght) { var messageEnvelope = serialiser.DeserialiseEnvelopedMessage(buffer, 0, lenght); peerReachabilityMatrix.TryGetValue(messageEnvelope.From, out var edgeMap); - if (messageEnvelope.KeyValuePairs != null && messageEnvelope.KeyValuePairs.TryGetValue(Constants.RoomName, out string roomName)) + if (messageEnvelope.KeyValuePairs != null) { - // find the room - rooms.TryGetValue(roomName, out var room); - var peerIdList = room.GetPeerIdList(); - foreach (var peerId in peerIdList) + if (messageEnvelope.KeyValuePairs.TryGetValue(Constants.RoomBroadcast, out string roomName)) { - if (edgeMap != null && edgeMap.TryGetValue(peerId, out _)) + // find the room + TryGetRoom(roomName, out var room); + var peerIdList = room.GetPeerIdList(); + foreach (var peerId in peerIdList) { - continue; - } + if (edgeMap != null && edgeMap.TryGetValue(peerId, out _)) + { + continue; + } - if (messageEnvelope.From != peerId) - RelayUdpMessage(peerId, buffer, 0, lenght); + if (messageEnvelope.From != peerId) + RelayUdpMessage(peerId, buffer, 0, lenght); + } } + + } + } + + private bool TryGetRoom(string roomName, out Room room) + { + return rooms.TryGetValue(roomName, out room); + } + + private bool TryRemoveRoom(string roomName, out Room room) + { + if( rooms.TryRemove(roomName, out room)) + { + roomPublishOperation.Signal(); + return true; } + return false; } - internal class Room where S : ISerializer, new() + + private bool TryAddRoom(string roomName, Room room) + { + if( rooms.TryAdd(roomName, room)) + { + roomPublishOperation.Signal(); + return true; + } + return false; + } + + public override void ShutdownServer() + { + batchBroadcastDispatcher.Abort(); + roomPublishOperation.Abort(); + + base.ShutdownServer(); + } + + internal class Room where T : ISerializer, new() { public readonly string RoomName; - private readonly SecureLobbyServer server; - private ConcurrentDictionary roomMates = new ConcurrentDictionary(); - private TaskCompletionSource PublishSignal = new TaskCompletionSource(); - public Room(string roomName, SecureLobbyServer server) + private readonly SecureLobbyServer server; + private ConcurrentDictionary roomMates = new ConcurrentDictionary(); + private AsyncDispatcher publishChangesOperation = new AsyncDispatcher(); + + SemaphoreSlim publishLock = new SemaphoreSlim(1,1); + public Room(string roomName, SecureLobbyServer server ) { RoomName = roomName; this.server = server; PublishRotune(); } - + public void LockRoom() + { + publishLock.Wait(); + } + public void UnlockRoom() + { + publishLock.Release(); + } private async void PublishRotune() { - while (true) + await publishChangesOperation.ExecuteBySignalEfficient(async () => { try { - await Task.Delay(1000).ConfigureAwait(false); - await PublishSignal.Task.ConfigureAwait(false); - - Interlocked.Exchange(ref PublishSignal, new TaskCompletionSource()); - - RoomPeerList rpl = GenerateRoomPeerList(); - MessageEnvelope message = PrepareMessage(rpl, out var toReturn); - lock (roomMates) + await publishLock.WaitAsync(); + try { + RoomPeerList rpl = GenerateRoomPeerList(); + MessageEnvelope message = PrepareMessage(rpl, out var toReturn); + foreach (var peer in roomMates) { server.SendAsyncMessage(peer.Key, message); } SharerdMemoryStreamPool.ReturnStreamStatic(toReturn); + } + finally + { + publishLock.Release(); + } + } - catch (Exception ex) { MiniLogger.Log(MiniLogger.LogLevel.Error, ex.Message); } + catch(Exception e) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, + $"An Error occured on Batch broadcast dispatch: " + + $"\n{e.InnerException.Message}" + + $"\n{e.InnerException.StackTrace}"); - } + PublishRotune(); + } + + }); + + + } + + public MessageEnvelope GetPeerListMsg() + { + RoomPeerList rpl = GenerateRoomPeerList(); + MessageEnvelope message = PrepareMessage(rpl, out var toReturn); + message.LockBytes(); + SharerdMemoryStreamPool.ReturnStreamStatic(toReturn); + return message; } private MessageEnvelope PrepareMessage(RoomPeerList rpl, out PooledMemoryStream returnAfterFinished) @@ -256,37 +587,56 @@ private RoomPeerList GenerateRoomPeerList() rpl.RoomName = RoomName; rpl.Peers = new PeerList(); rpl.Peers.PeerIds = new Dictionary(); - + + foreach (var item in roomMates) { var ep = server.GetIPEndPoint(item.Key); var peerInfo = new PeerInfo() { Address = ep.Address.GetAddressBytes(), - Port = (ushort)ep.Port + Port = (ushort)ep.Port, + RegisteryTime = item.Value + }; rpl.Peers.PeerIds[item.Key] = peerInfo; } + + return rpl; } public void Add(Guid peerId) { - if (roomMates.TryAdd(peerId, null)) - PublishSignal.TrySetResult(true); + if (roomMates.TryAdd(peerId, server.GetTime())) + publishChangesOperation.Signal(); } - public void Remove(Guid peerId) + public int Remove(Guid peerId) { if (roomMates.TryRemove(peerId, out _)) - PublishSignal.TrySetResult(true); + { + publishChangesOperation.Signal(); + } + + + var cnt = roomMates.Count; + return cnt; } internal ICollection GetPeerIdList() { - return roomMates.Keys; + return roomMates.Keys; + } + + + public void Close() + { + publishChangesOperation.Abort(); } } + + } } diff --git a/NetworkLibrary/TCP/AES/AesTcpClient.cs b/NetworkLibrary/TCP/AES/AesTcpClient.cs index 9796818..52b463d 100644 --- a/NetworkLibrary/TCP/AES/AesTcpClient.cs +++ b/NetworkLibrary/TCP/AES/AesTcpClient.cs @@ -14,7 +14,7 @@ public class AesTcpClient : AsyncTpcClient { private AesTcpSession sessionInternal; private ConcurrentAesAlgorithm algorithm; - + private Socket connectedSocket; public AesTcpClient(ConcurrentAesAlgorithm algorithm) { this.algorithm = algorithm; @@ -22,11 +22,21 @@ public AesTcpClient(ConcurrentAesAlgorithm algorithm) public AesTcpClient(Socket clientSocket, ConcurrentAesAlgorithm algorithm) { - this.algorithm = algorithm; SetConnectedSocket(clientSocket,ScatterGatherConfig.UseBuffer); } + internal AesTcpClient(ConcurrentAesAlgorithm algorithm,Socket clientSocket) + { + this.connectedSocket = clientSocket; + this.algorithm = algorithm; + } + + internal void Start() + { + SetConnectedSocket(connectedSocket, ScatterGatherConfig.UseBuffer); + } + private protected override IAsyncSession CreateSession(SocketAsyncEventArgs e, Guid sessionId) { var session = new AesTcpSession(e, sessionId,algorithm); @@ -44,17 +54,17 @@ private protected override IAsyncSession CreateSession(SocketAsyncEventArgs e, G // this.algorithm = algorithm; // sessionInternal.Algorithm = algorithm; //} - private protected override IAsyncSession CreateSession(Socket socket, Guid sessionId) - { - var session = new AesTcpSession(socket, sessionId, algorithm); - session.socketSendBufferSize = SocketSendBufferSize; - session.SocketRecieveBufferSize = SocketRecieveBufferSize; - session.MaxIndexedMemory = MaxIndexedMemory; - session.DropOnCongestion = DropOnCongestion; - session.UseQueue = false; - sessionInternal = session; - return session; - } + //private protected override IAsyncSession CreateSession(Socket socket, Guid sessionId) + //{ + // var session = new AesTcpSession(socket, sessionId, algorithm); + // session.socketSendBufferSize = SocketSendBufferSize; + // session.SocketRecieveBufferSize = SocketRecieveBufferSize; + // session.MaxIndexedMemory = MaxIndexedMemory; + // session.DropOnCongestion = DropOnCongestion; + // session.UseQueue = false; + // sessionInternal = session; + // return session; + //} public void SendAsync(byte[] data1, int offset1, int count1, byte[] data2, int offset2, int count2) { diff --git a/NetworkLibrary/TCP/Base/AsyncTcpClient.cs b/NetworkLibrary/TCP/Base/AsyncTcpClient.cs index fe92d28..3445627 100644 --- a/NetworkLibrary/TCP/Base/AsyncTcpClient.cs +++ b/NetworkLibrary/TCP/Base/AsyncTcpClient.cs @@ -17,14 +17,16 @@ public class AsyncTpcClient : TcpClientBase, IDisposable public AsyncTpcClient() { } - protected void SetConnectedSocket(Socket clientSocket, ScatterGatherConfig config) + internal protected virtual void SetConnectedSocket(Socket clientSocket, ScatterGatherConfig config) { this.GatherConfig = config; this.clientSocket = clientSocket; IsConnected = true; var Id = Guid.NewGuid(); - session = CreateSession(clientSocket, Id); + var ea = new SocketAsyncEventArgs(); + ea.AcceptSocket = clientSocket; + session = CreateSession(ea, Id); session.OnBytesRecieved += (sessionId, bytes, offset, count) => HandleBytesRecieved(bytes, offset, count); session.OnSessionClosed += (sessionId) => OnDisconnected?.Invoke(); @@ -118,20 +120,20 @@ private protected virtual IAsyncSession CreateSession(SocketAsyncEventArgs e, Gu ses.UseQueue = false; return ses; } - private protected virtual IAsyncSession CreateSession(Socket e, Guid sessionId) - { - var ses = new TcpSession(e, sessionId); - ses.socketSendBufferSize = SocketSendBufferSize; - ses.SocketRecieveBufferSize = SocketRecieveBufferSize; - ses.MaxIndexedMemory = MaxIndexedMemory; - ses.DropOnCongestion = DropOnCongestion; - - if (GatherConfig == ScatterGatherConfig.UseQueue) - ses.UseQueue = true; - else - ses.UseQueue = false; - return ses; - } + //private protected virtual IAsyncSession CreateSession(Socket e, Guid sessionId) + //{ + // var ses = new TcpSession(e, sessionId); + // ses.socketSendBufferSize = SocketSendBufferSize; + // ses.SocketRecieveBufferSize = SocketRecieveBufferSize; + // ses.MaxIndexedMemory = MaxIndexedMemory; + // ses.DropOnCongestion = DropOnCongestion; + + // if (GatherConfig == ScatterGatherConfig.UseQueue) + // ses.UseQueue = true; + // else + // ses.UseQueue = false; + // return ses; + //} #endregion #region Send & Receive diff --git a/NetworkLibrary/TCP/Base/AsyncTcpServer.cs b/NetworkLibrary/TCP/Base/AsyncTcpServer.cs index 17ea95b..5592223 100644 --- a/NetworkLibrary/TCP/Base/AsyncTcpServer.cs +++ b/NetworkLibrary/TCP/Base/AsyncTcpServer.cs @@ -188,7 +188,11 @@ public void SendBytesToClient(Guid id, byte[] bytes, int offset, int count) if (Sessions.TryGetValue(id, out var session)) session.SendAsync(bytes, offset, count); } - + internal void SendBytesToClientDirect(Guid id, byte[] bytes, int offset, int count) + { + if (Sessions.TryGetValue(id, out var session)) + ((TcpSession)session).SendDirect(bytes, offset, count); + } protected virtual void HandleBytesReceived(Guid guid, byte[] bytes, int offset, int count) { OnBytesReceived?.Invoke(guid, bytes, offset, count); diff --git a/NetworkLibrary/TCP/Base/Core/IAsyncSession.cs b/NetworkLibrary/TCP/Base/Core/IAsyncSession.cs index 8f3008b..ecb6f2d 100644 --- a/NetworkLibrary/TCP/Base/Core/IAsyncSession.cs +++ b/NetworkLibrary/TCP/Base/Core/IAsyncSession.cs @@ -1,5 +1,6 @@ using NetworkLibrary.Components.Statistics; using System; +using System.Collections.Generic; using System.Net; namespace NetworkLibrary.TCP.Base @@ -35,6 +36,8 @@ internal interface IAsyncSession : IDisposable /// void SendAsync(byte[] buffer, int offset, int count); + void SendAsync(List> batch); + /// /// Starts the session /// diff --git a/NetworkLibrary/TCP/Base/TcpSession.cs b/NetworkLibrary/TCP/Base/TcpSession.cs index 043d9c9..2e86f29 100644 --- a/NetworkLibrary/TCP/Base/TcpSession.cs +++ b/NetworkLibrary/TCP/Base/TcpSession.cs @@ -3,6 +3,7 @@ using NetworkLibrary.Components.Statistics; using NetworkLibrary.Utils; using System; +using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; @@ -46,9 +47,9 @@ internal class TcpSession : IAsyncSession private long totalMessageReceived = 0; private long totalMsgReceivedPrev; private long totalMSgSentPrev; -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER -// Memory receiveMemory; -//#endif + //#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + // Memory receiveMemory; + //#endif #endregion public IPEndPoint RemoteEndpoint => (IPEndPoint)sessionSocket.RemoteEndPoint; @@ -72,7 +73,7 @@ public virtual void StartSession() InitialiseSendArgs(); InitialiseReceiveArgs(); messageBuffer = CreateMessageQueue(); - ThreadPool.UnsafeQueueUserWorkItem((e) => Receive(),null); + ThreadPool.UnsafeQueueUserWorkItem((e) => Receive(), null); } protected virtual void ConfigureSocket() @@ -83,9 +84,7 @@ protected virtual void ConfigureSocket() protected virtual void ConfigureBuffers() { recieveBuffer = BufferPool.RentBuffer(socketSendBufferSize); -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER -// receiveMemory= new Memory(recieveBuffer); -//#endif + if (UseQueue) sendBuffer = BufferPool.RentBuffer(SocketRecieveBufferSize); } @@ -124,10 +123,7 @@ protected virtual IMessageQueue CreateMessageQueue() #region Recieve protected virtual void Receive() { -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER -// ReceiveModern(); -// return; -//#endif + if (IsSessionClosing()) { ReleaseReceiveResourcesIdempotent(); @@ -148,76 +144,55 @@ protected virtual void Receive() } -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - -// private async void ReceiveModern() -// { -// try -// { -// while (true) -// { -// if (IsSessionClosing()) -// { -// ReleaseReceiveResourcesIdempotent(); -// return; -// } - -// int amountReceived = await sessionSocket.ReceiveAsync(receiveMemory, -// SocketFlags.None).ConfigureAwait(false); - -// if (IsSessionClosing()) -// { -// ReleaseReceiveResourcesIdempotent(); -// return; -// } -// if (amountReceived == 0) -// { -// Disconnect(); -// ReleaseReceiveResourcesIdempotent(); -// return; -// } -// totalBytesReceived += amountReceived; -// HandleReceived(recieveBuffer, 0, amountReceived); -// } -// } -// catch (Exception e) -// { -// if (!IsSessionClosing()) -// { -// MiniLogger.Log(MiniLogger.LogLevel.Error, -// "While receiving on tcp session error occured;" +e.Message); -// EndSession(); -// } -// } -// } -//#endif + private void BytesRecieved(object sender, SocketAsyncEventArgs e) { - if (IsSessionClosing()) - { - ReleaseReceiveResourcesIdempotent(); - return; - } - - if (e.SocketError != SocketError.Success) - { - HandleError(e, "while recieving from "); - Disconnect(); - ReleaseReceiveResourcesIdempotent(); - return; - } - else if (e.BytesTransferred == 0) + while (true) { - Disconnect(); - ReleaseReceiveResourcesIdempotent(); - return; - } - totalBytesReceived += e.BytesTransferred; + if (IsSessionClosing()) + { + ReleaseReceiveResourcesIdempotent(); + return; + } - HandleReceived(e.Buffer, e.Offset, e.BytesTransferred); - Receive(); + if (e.SocketError != SocketError.Success) + { + HandleError(e, "while recieving from "); + Disconnect(); + ReleaseReceiveResourcesIdempotent(); + return; + } + else if (e.BytesTransferred == 0) + { + Disconnect(); + ReleaseReceiveResourcesIdempotent(); + return; + } + totalBytesReceived += e.BytesTransferred; + try + { + HandleReceived(e.Buffer, e.Offset, e.BytesTransferred); + } + catch (Exception ex) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, ex.Message + "\n" + ex.StackTrace); + EndSession(); + } + //Receive(); + try + { + ClientRecieveEventArg.SetBuffer(0, ClientRecieveEventArg.Buffer.Length); + if (sessionSocket.ReceiveAsync(ClientRecieveEventArg)) + { + return; + } + } + catch (Exception ex) + when (ex is ObjectDisposedException || ex is NullReferenceException) + { ReleaseReceiveResourcesIdempotent(); } + } } protected virtual void HandleReceived(byte[] buffer, int offset, int count) @@ -229,6 +204,83 @@ protected virtual void HandleReceived(byte[] buffer, int offset, int count) #endregion Recieve #region Send + internal void SendDirect(byte[] b, int o, int c) + { + if (IsSessionClosing()) + return; + try + { + sessionSocket.Send(b, o, c, SocketFlags.None); + } + catch (Exception e) + { + if (!IsSessionClosing()) + MiniLogger.Log(MiniLogger.LogLevel.Error, + "Unexpected error while sending async with tcp session" + e.Message + "Trace " + e.StackTrace); + } + } + public void SendAsync(List> batch) + { + if (IsSessionClosing()) + return; + try + { + SendAsync_(batch); + } + catch (Exception e) + { + if (!IsSessionClosing()) + MiniLogger.Log(MiniLogger.LogLevel.Error, + "Unexpected error while sending async with ssl session" + e.Message + "Trace " + e.StackTrace); + } + } + + private void SendAsync_(List> batch) + { + enqueueLock.Take(); + if (IsSessionClosing()) + { + ReleaseSendResourcesIdempotent(); + return; + } + if (SendSemaphore.IsTaken()) + { + foreach (var segment in batch) + { + if (!messageBuffer.TryEnqueueMessage(segment.Array, segment.Offset, segment.Count)) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "Message is too large to fit on buffer"); + EndSession(); + return; + } + } + } + enqueueLock.Release(); + + if (DropOnCongestion && SendSemaphore.IsTaken()) return; + + SendSemaphore.Take(); + if (IsSessionClosing()) + { + ReleaseSendResourcesIdempotent(); + SendSemaphore.Release(); + return; + } + + foreach (var segment in batch) + { + // you have to push it to queue because queue also does the processing. + if (!messageBuffer.TryEnqueueMessage(segment.Array, segment.Offset, segment.Count)) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "Message is too large to fit on buffer"); + EndSession(); + return; + } + } + + messageBuffer.TryFlushQueue(ref sendBuffer, 0, out int amountWritten); + FlushSendBuffer(0, amountWritten); + } public virtual void SendAsync(byte[] bytes) { if (IsSessionClosing()) @@ -329,10 +381,7 @@ void SendOrEnqueue(byte[] bytes) [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void FlushSendBuffer(int offset, int count) { -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - //ThreadPool.UnsafeQueueUserWorkItem((e) => SendModern(offset,count),null); - //return; -//#endif + try { totalBytesSend += count; @@ -345,70 +394,6 @@ protected void FlushSendBuffer(int offset, int count) } catch { EndSession(); ReleaseSendResourcesIdempotent(); } } -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER -// private async void SendModern(int offset, int count) -// { -// try -// { - - -// Top: -// await sessionSocket.SendAsync(new ReadOnlyMemory(sendBuffer, offset, count), -// SocketFlags.None).ConfigureAwait(false); - -// if (IsSessionClosing()) -// { -// SendSemaphore.Release(); -// ReleaseSendResourcesIdempotent(); -// return; -// } - -// if (messageBuffer.TryFlushQueue(ref sendBuffer, 0, out int amountWritten)) -// { -// //FlushSendBuffer(0, amountWritten); -// offset = 0; -// count = amountWritten; -// goto Top; -// } -// else -// { -// bool flushAgain = false; -// // here it means queue was empty and there was nothing to flush. -// // but this check is clearly not atomic, if during the couple cycles in between something is enqueued, -// // i have to flush that part ,or it will stuck at queue since consumer is exiting. - -// enqueueLock.Take(); -// if (!messageBuffer.IsEmpty()) -// { -// flushAgain = true; -// enqueueLock.Release(); -// } -// else -// { -// messageBuffer.Flush(); - -// SendSemaphore.Release(); -// enqueueLock.Release(); -// if (IsSessionClosing()) -// { -// ReleaseSendResourcesIdempotent(); -// } -// return; -// } - -// if (flushAgain && messageBuffer.TryFlushQueue(ref sendBuffer, 0, out amountWritten)) -// { -// // FlushSendBuffer(0, amountWritten); -// offset = 0; -// count = amountWritten; -// goto Top; -// } -// } -// } -// catch { EndSession(); ReleaseSendResourcesIdempotent(); } - -// } -//#endif [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SendComplete(object ignored, SocketAsyncEventArgs e) @@ -481,7 +466,7 @@ private void SendComplete(object ignored, SocketAsyncEventArgs e) { if (!IsSessionClosing()) { - MiniLogger.Log(MiniLogger.LogLevel.Error,"Error on sent callback tcp session"+ ex.Message); + MiniLogger.Log(MiniLogger.LogLevel.Error, "Error on sent callback tcp session" + ex.Message); EndSession(); } } @@ -574,7 +559,7 @@ protected virtual void ReleaseReceiveResources() BufferPool.ReturnBuffer(recieveBuffer); } catch { } - + } protected void DcAndDispose() diff --git a/NetworkLibrary/TCP/Generic/GenericSecureSession.cs b/NetworkLibrary/TCP/Generic/GenericSecureSession.cs index 75b0fae..74c1291 100644 --- a/NetworkLibrary/TCP/Generic/GenericSecureSession.cs +++ b/NetworkLibrary/TCP/Generic/GenericSecureSession.cs @@ -57,7 +57,7 @@ public void SendAsync(T message) { SendAsyncInternal(message); } - catch { if (!IsSessionClosing()) throw; } + catch { if (!IsSessionClosing()) { EndSession(); throw; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/NetworkLibrary/TCP/SSL/SslClient.cs b/NetworkLibrary/TCP/SSL/SslClient.cs index 56039b6..765ba84 100644 --- a/NetworkLibrary/TCP/SSL/SslClient.cs +++ b/NetworkLibrary/TCP/SSL/SslClient.cs @@ -53,6 +53,7 @@ public SslClient() private Socket GetSocket() { Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); return socket; } @@ -91,6 +92,20 @@ public override Task ConnectAsyncAwaitable(string ip, int port) earg.RemoteEndPoint = new IPEndPoint(IPAddress.Parse(ip), port); earg.Completed += (ignored, arg) => { HandleResult(arg); }; + Task.Delay(10000).ContinueWith(t => + { + if (IsConnecting) + { + try + { + clientSocket.Close(); + + } + catch { } + tcs.TrySetResult(false); + } + }, TaskScheduler.Default); + if (!clientSocket.ConnectAsync(earg)) { HandleResult(earg); @@ -144,6 +159,23 @@ public override void ConnectAsync(string IP, int port) private void Connected(string domainName, Socket clientSocket) { + clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + + int dscpValue = 40; // Critical services + byte tos = (byte)(dscpValue << 2); // Shift left by 2 bits + clientSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.TypeOfService, tos); + +#if NET5_0_OR_GREATER + const int TcpKeepAliveTime = 15; // Start keepalive after 60 seconds + const int TcpKeepAliveInterval = 5; // Send probes every 10 seconds + const int TcpKeepAliveProbes = 2; // Retry 5 times before dropping + + clientSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, TcpKeepAliveTime); + clientSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, TcpKeepAliveInterval); + clientSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, TcpKeepAliveProbes); +#endif + + sslStream = new SslStream(new NetworkStream(clientSocket, true), false, ValidateCeriticate); sslStream.AuthenticateAsClient(domainName, new X509CertificateCollection(new[] { certificate }), System.Security.Authentication.SslProtocols.None, true); diff --git a/NetworkLibrary/TCP/SSL/SslServer.cs b/NetworkLibrary/TCP/SSL/SslServer.cs index c41e441..898db79 100644 --- a/NetworkLibrary/TCP/SSL/SslServer.cs +++ b/NetworkLibrary/TCP/SSL/SslServer.cs @@ -4,6 +4,7 @@ using NetworkLibrary.Utils; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Net; using System.Net.Security; using System.Net.Sockets; @@ -43,14 +44,16 @@ public class SslServer : TcpServerBase public int SessionCount => Sessions.Count; - private protected ConcurrentDictionary Sessions = new ConcurrentDictionary(); + internal ConcurrentDictionary Sessions = new ConcurrentDictionary(); internal ConcurrentDictionary Stats { get; } = new ConcurrentDictionary(); private Socket serverSocket; private X509Certificate2 certificate; private TcpServerStatisticsPublisher statisticsPublisher; - + const int TcpKeepAliveTime = 30; // Start keepalive after 30 seconds + const int TcpKeepAliveInterval = 10; // Send probes every 10 seconds + const int TcpKeepAliveProbes = 2; // Retry 3 times before dropping public SslServer(int port, X509Certificate2 certificate) { ServerPort = port; @@ -77,6 +80,18 @@ public SslServer(int port) public override void StartServer() { serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); + serverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + + int dscpValue = 40; // Critical services + byte tos = (byte)(dscpValue << 2); // Shift left by 2 bits + serverSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.TypeOfService, tos); + +#if NET5_0_OR_GREATER + + serverSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, TcpKeepAliveTime); + serverSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, TcpKeepAliveInterval); + serverSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, TcpKeepAliveProbes); +#endif serverSocket.ReceiveBufferSize = ServerSockerReceiveBufferSize; serverSocket.Bind(new IPEndPoint(IPAddress.Any, ServerPort)); @@ -115,6 +130,18 @@ private void Accepted(object sender, SocketAsyncEventArgs acceptedArg) return; } + acceptedArg.AcceptSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + + int dscpValue = 40; // Critical services + byte tos = (byte)(dscpValue << 2); // Shift left by 2 bits + acceptedArg.AcceptSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.TypeOfService, tos); + +#if NET5_0_OR_GREATER + + acceptedArg.AcceptSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, TcpKeepAliveTime); + acceptedArg.AcceptSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, TcpKeepAliveInterval); + acceptedArg.AcceptSocket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, TcpKeepAliveProbes); +#endif if (!ValidateConnection(acceptedArg.AcceptSocket)) { return; @@ -217,6 +244,12 @@ public override void SendBytesToClient(Guid clientId, byte[] bytes) session.SendAsync(bytes); } + public void SendSegmentsToClient(Guid clientId, List> segments) + { + if (Sessions.TryGetValue(clientId, out var session)) + session.SendAsync(segments); + } + public void SendBytesToClient(Guid clientId, byte[] bytes, int offset, int count) { if (Sessions.TryGetValue(clientId, out var session)) diff --git a/NetworkLibrary/TCP/SSL/SslSession.cs b/NetworkLibrary/TCP/SSL/SslSession.cs index fa0cc2a..72c2b80 100644 --- a/NetworkLibrary/TCP/SSL/SslSession.cs +++ b/NetworkLibrary/TCP/SSL/SslSession.cs @@ -4,6 +4,8 @@ using NetworkLibrary.TCP.Base; using NetworkLibrary.Utils; using System; +using System.Collections.Generic; +using System.Drawing; using System.Net; using System.Net.Security; using System.Runtime.CompilerServices; @@ -26,9 +28,7 @@ public class SslSession : IAsyncSession protected Spinlock enqueueLock = new Spinlock(); protected SslStream sessionStream; protected byte[] receiveBuffer; -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER -// protected Memory receiveMemory; -//#endif + protected byte[] sendBuffer; protected Guid sessionId; protected IPEndPoint RemoteEP; @@ -64,12 +64,10 @@ public void StartSession() protected virtual void ConfigureBuffers() { - receiveBuffer = /*new byte[ReceiveBufferSize];*/ BufferPool.RentBuffer(ReceiveBufferSize); - -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - -// receiveMemory = new Memory(receiveBuffer); -//#endif + receiveBuffer = BufferPool.RentBuffer(ReceiveBufferSize); + if (UseQueue) + sendBuffer = BufferPool.RentBuffer(SendBufferSize); + receiveBuffer = BufferPool.RentBuffer(ReceiveBufferSize); if (UseQueue) sendBuffer = BufferPool.RentBuffer(SendBufferSize); @@ -84,22 +82,84 @@ protected virtual IMessageQueue CreateMessageQueue() } + public void SendAsync(List> batch) + { + if (IsSessionClosing()) + return; + try + { + SendAsyncInternal( batch); + } + catch (Exception e) + { + if (!IsSessionClosing()) + MiniLogger.Log(MiniLogger.LogLevel.Error, + "Unexpected error while sending async with ssl session" + e.Message + "Trace " + e.StackTrace); + } + } + private void SendAsyncInternal(List> batch) + { + enqueueLock.Take(); + if (IsSessionClosing()) + { + ReleaseSendResourcesIdempotent(); + return; + } + if (SendSemaphore.IsTaken()) + { + + if(!messageQueue.TryEnqueueMessage(batch)) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "Message is too large to fit on buffer"); + EndSession(); + return; + } + + } + enqueueLock.Release(); + + if (DropOnCongestion && SendSemaphore.IsTaken()) return; + + SendSemaphore.Take(); + if (IsSessionClosing()) + { + ReleaseSendResourcesIdempotent(); + SendSemaphore.Release(); + return; + } + + + // you have to push it to queue because queue also does the processing. + if (!messageQueue.TryEnqueueMessage(batch)) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, "Message is too large to fit on buffer"); + EndSession(); + return; + } + + + FlushAndSend(); + } + + public void SendAsync(byte[] buffer, int offset, int count) { if (IsSessionClosing()) return; try { - SendAsync_(buffer, offset, count); + SendAsyncInternal(buffer, offset, count); } catch(Exception e) { if (!IsSessionClosing()) MiniLogger.Log(MiniLogger.LogLevel.Error, "Unexpected error while sending async with ssl session" + e.Message+"Trace " +e.StackTrace); + EndSession(); + throw; } } - private void SendAsync_(byte[] buffer, int offset, int count) + private void SendAsyncInternal(byte[] buffer, int offset, int count) { enqueueLock.Take(); if (IsSessionClosing()) @@ -143,16 +203,18 @@ public void SendAsync(byte[] buffer) return; try { - SendAsync_(buffer); + SendAsyncInternal(buffer); } catch (Exception e) { if (!IsSessionClosing()) MiniLogger.Log(MiniLogger.LogLevel.Error, "Unexpected error while sending async with ssl session" + e.Message + "Trace " + e.StackTrace); + EndSession(); + throw; } } - private void SendAsync_(byte[] buffer) + private void SendAsyncInternal(byte[] buffer) { enqueueLock.Take(); if (IsSessionClosing()) @@ -191,8 +253,6 @@ private void SendAsync_(byte[] buffer) // this can only be called inside send lock critical section protected void FlushAndSend() { - //ThreadPool.UnsafeQueueUserWorkItem((s) => - //{ try { messageQueue.TryFlushQueue(ref sendBuffer, 0, out int amountWritten); @@ -203,17 +263,10 @@ protected void FlushAndSend() if (!IsSessionClosing()) throw; } - - // }, null); - } + protected void WriteOnSessionStream(int count) { -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER -// WriteModern(count); -// return; -//#endif - try { sessionStream.BeginWrite(sendBuffer, 0, count, SentInternal, null); @@ -225,68 +278,6 @@ protected void WriteOnSessionStream(int count) totalBytesSend += count; } -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - -// private async void WriteModern(int count) -// { -// try -// { -// //somehow faster than while loop... -// Top: -// totalBytesSend += count; -// await sessionStream.WriteAsync(new ReadOnlyMemory(sendBuffer, 0, count)).ConfigureAwait(false); - -// if (IsSessionClosing()) -// { -// ReleaseSendResourcesIdempotent(); -// return; -// } -// if (messageQueue.TryFlushQueue(ref sendBuffer, 0, out int amountWritten)) -// { -// count = amountWritten; -// goto Top; - -// } - -// // here there was nothing to flush -// bool flush = false; - -// enqueueLock.Take(); -// // ask again safely -// if (messageQueue.IsEmpty()) -// { -// messageQueue.Flush(); - -// SendSemaphore.Release(); -// enqueueLock.Release(); -// if (IsSessionClosing()) -// ReleaseSendResourcesIdempotent(); -// return; -// } -// else -// { -// flush = true; - -// } -// enqueueLock.Release(); - -// // something got into queue just before i exit, we need to flush it -// if (flush) -// { -// if (messageQueue.TryFlushQueue(ref sendBuffer, 0, out int amountWritten_)) -// { -// count = amountWritten_; -// goto Top; -// } -// } -// } -// catch (Exception e) -// { -// HandleError("Error on sent callback ssl", e); -// } -// } -//#endif - private void SentInternal(IAsyncResult ar) { if (ar.CompletedSynchronously) @@ -365,10 +356,6 @@ private void Sent(IAsyncResult ar) protected virtual void Receive() { -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER -// ReceiveNew(); -// return; -//#endif if (IsSessionClosing()) { ReleaseReceiveResourcesIdempotent(); @@ -384,39 +371,7 @@ protected virtual void Receive() ReleaseReceiveResourcesIdempotent(); } } -//#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - -// private async void ReceiveNew() -// { -// try -// { -// while (true) -// { -// if (IsSessionClosing()) -// { -// ReleaseReceiveResourcesIdempotent(); -// return; -// } -// var amountRead = await sessionStream.ReadAsync(receiveMemory).ConfigureAwait(false); -// if (amountRead > 0) -// { -// HandleReceived(receiveBuffer, 0, amountRead); -// } -// else -// { -// EndSession(); -// ReleaseReceiveResourcesIdempotent(); -// } -// totalBytesReceived += amountRead; -// } -// } -// catch (Exception ex) -// { -// HandleError("White receiving from SSL socket an error occurred", ex); -// ReleaseReceiveResourcesIdempotent(); -// } -// } -//#endif + protected virtual void Received(IAsyncResult ar) { if (IsSessionClosing()) @@ -440,7 +395,15 @@ protected virtual void Received(IAsyncResult ar) if (amountRead > 0) { - HandleReceived(receiveBuffer, 0, amountRead); + try + { + HandleReceived(receiveBuffer, 0, amountRead); + } + catch (Exception e) + { + MiniLogger.Log(MiniLogger.LogLevel.Error, e.Message + "\n" + e.StackTrace); + EndSession(); + } } else { @@ -462,7 +425,6 @@ protected virtual void HandleReceived(byte[] buffer, int offset, int count) { totalMessageReceived++; OnBytesRecieved?.Invoke(sessionId, buffer, offset, count); - } #region Closure & Disposal @@ -501,7 +463,7 @@ public void EndSession() } - int sendResReleased = 0; + int sendResReleased = 0; protected void ReleaseSendResourcesIdempotent() { if (Interlocked.CompareExchange(ref sendResReleased, 1, 0) == 0) diff --git a/NetworkLibrary/UDP/AsyncUdpClient.cs b/NetworkLibrary/UDP/AsyncUdpClient.cs index 30a57f1..d3688d5 100644 --- a/NetworkLibrary/UDP/AsyncUdpClient.cs +++ b/NetworkLibrary/UDP/AsyncUdpClient.cs @@ -57,8 +57,10 @@ public AsyncUdpClient() clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); - //clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - //clientSocket.SetSocketOption(SocketOptionLevel.Udp, SocketOptionName.PacketInformation, true); + + int dscpValue = 40; // Critical services + byte tos = (byte)(dscpValue << 2); // Shift left by 2 bits + clientSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.TypeOfService, tos); clientSocket.ReceiveBufferSize = ReceiveBufferSize; clientSocket.SendBufferSize = SocketSendBufferSize; diff --git a/NetworkLibrary/UDP/AsyncUdpServer.cs b/NetworkLibrary/UDP/AsyncUdpServer.cs index 1d8baf4..eb10cbe 100644 --- a/NetworkLibrary/UDP/AsyncUdpServer.cs +++ b/NetworkLibrary/UDP/AsyncUdpServer.cs @@ -18,6 +18,8 @@ public class AsyncUdpServer:IDisposable public BytesRecieved OnBytesRecieved; public int ClientReceiveBufferSize = 65000; + public Action ClientDisconnected; + public int SocketReceiveBufferSize { get => receiveBufferSize; @@ -57,7 +59,9 @@ public AsyncUdpServer(int port = 20008) ServerSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); // Not compatible with Unity.. //ServerSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); - + int dscpValue = 40; // Critical services + byte tos = (byte)(dscpValue << 2); // Shift left by 2 bits + ServerSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.TypeOfService, tos); ServerSocket.ReceiveBufferSize = SocketReceiveBufferSize; ServerSocket.SendBufferSize = SocketSendBufferSize; @@ -108,20 +112,31 @@ private void Receive(SocketAsyncEventArgs e) } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Received(object sender, SocketAsyncEventArgs e) { - if (e.SocketError != SocketError.Success) + while (true) { - StartReceiveSentinel(); - e.Dispose(); - return; - } + if (e.SocketError != SocketError.Success) + { + StartReceiveSentinel(); + ClientDisconnected?.Invoke(e.RemoteEndPoint as IPEndPoint); + e.Dispose(); + return; + } - HandleMessage(e); - e.RemoteEndPoint = serverEndpoint; - e.SetBuffer(0, ClientReceiveBufferSize); - Receive(e); + HandleMessage(e); + e.RemoteEndPoint = serverEndpoint; + e.SetBuffer(0, ClientReceiveBufferSize); + //Receive(e); + try + { + if (ServerSocket.ReceiveFromAsync(e)) + { + return; + } + } + catch (Exception ex) when (ex is ObjectDisposedException) { } + } } private void HandleMessage(SocketAsyncEventArgs e) diff --git a/NetworkLibrary/UDP/AsyncUdpServerLitecs.cs b/NetworkLibrary/UDP/AsyncUdpServerLitecs.cs index 5da9a9a..71ad99a 100644 --- a/NetworkLibrary/UDP/AsyncUdpServerLitecs.cs +++ b/NetworkLibrary/UDP/AsyncUdpServerLitecs.cs @@ -52,6 +52,9 @@ public AsyncUdpServerLite(int port) // Not compatible with Unity.. //ServerSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); + int dscpValue = 40; // Critical services + byte tos = (byte)(dscpValue << 2); // Shift left by 2 bits + ServerSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.TypeOfService, tos); ServerSocket.ReceiveBufferSize = SocketReceiveBufferSize; ServerSocket.SendBufferSize = SocketSendBufferSize; diff --git a/NetworkLibrary/UDP/Jumbo/JumboModule.cs b/NetworkLibrary/UDP/Jumbo/JumboModule.cs index aeb6842..95b5b0c 100644 --- a/NetworkLibrary/UDP/Jumbo/JumboModule.cs +++ b/NetworkLibrary/UDP/Jumbo/JumboModule.cs @@ -10,10 +10,11 @@ public class JumboModule Sender sender; public Action SendToSocket; public Action MessageReceived; - public JumboModule() + public JumboModule(int reserveforPrefix = 38) { this.receiver = new Receiver(); this.sender = new Sender(); + sender.ReserveForPrefix = reserveforPrefix; sender.OnSend = SendBytesToSocket; receiver.OnMessageExtracted = HandleExtractedMessage; } diff --git a/NetworkLibrary/UDP/Jumbo/Sender.cs b/NetworkLibrary/UDP/Jumbo/Sender.cs index 0f5ba19..245ffbe 100644 --- a/NetworkLibrary/UDP/Jumbo/Sender.cs +++ b/NetworkLibrary/UDP/Jumbo/Sender.cs @@ -27,81 +27,10 @@ private static byte[] GetBuffer() public Action OnSend; public void ProcessBytes(byte[] buffer, int offset, int count) { - //if (count < 256000) - //{ - // ProcessBytesUnsafe(buffer, offset, count); - //} - //else - { - ProcessBytesSafe(buffer, offset, count); - } - } - private unsafe void ProcessBytesUnsafe(byte[] buffer, int offset, int count) - { - var b = stackalloc byte[count]; - fixed (byte* message_ = &buffer[offset]) - Buffer.MemoryCopy(message_, b, count, count); - - - int totalNumSeq = count / FragmentSize; - if (count % FragmentSize != 0) - { - totalNumSeq++; - } - - var msgNo = Interlocked.Increment(ref currentMsgNo); - byte curresntSeq = 1; - - var tempBuff = GetBuffer(); - while (count > FragmentSize) - { - int offset_ = ReserveForPrefix; - int tempbuffCnt = 0; - - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; - WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); - curresntSeq++; - tempbuffCnt += offset_; - - // ByteCopy.BlockCopy(buffer, offset, tempBuff, offset_, fragmentsize); - fixed (byte* dest = &tempBuff[offset_]) - Buffer.MemoryCopy(b+offset, dest, FragmentSize, FragmentSize); - offset += FragmentSize; - count -= FragmentSize; - tempbuffCnt += FragmentSize; - var cc = tempBuff[tempbuffCnt-1]; - var a = buffer[offset]; - - OnSend?.Invoke(tempBuff, 0, tempbuffCnt); - - } - if (count > 0) - { - int offset_ = ReserveForPrefix; - int tempbuffCnt = 0; - - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; - WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); - curresntSeq++; - tempbuffCnt += offset_; - - var a = buffer[offset - 1]; - a = buffer[offset]; - a = buffer[offset + 1]; - - // ByteCopy.BlockCopy(buffer, offset, tempBuff, offset_, count); - fixed (byte* dest = &tempBuff[offset_]) - Buffer.MemoryCopy(b + offset, dest, count, count); - tempbuffCnt += count; - OnSend?.Invoke(tempBuff, 0, tempbuffCnt); - } + + ProcessBytesSafe(buffer, offset, count); } + private void ProcessBytesSafe(byte[] buffer, int offset, int count) { @@ -124,10 +53,7 @@ private void ProcessBytesSafe(byte[] buffer, int offset, int count) int offset_ = ReserveForPrefix; int tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; @@ -148,10 +74,7 @@ private void ProcessBytesSafe(byte[] buffer, int offset, int count) int offset_ = ReserveForPrefix; int tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; @@ -181,10 +104,7 @@ public void ProcessBytes(in Segment first, in Segment second) int offset_ = ReserveForPrefix; int tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; @@ -208,10 +128,7 @@ public void ProcessBytes(in Segment first, in Segment second) offset_ = ReserveForPrefix; tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; @@ -230,10 +147,7 @@ public void ProcessBytes(in Segment first, in Segment second) offset_ = ReserveForPrefix; tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; @@ -262,10 +176,7 @@ public unsafe void ProcessBytes(in SegmentUnsafe first, in Segment second) int offset_ = ReserveForPrefix; int tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; @@ -293,10 +204,7 @@ public unsafe void ProcessBytes(in SegmentUnsafe first, in Segment second) offset_ = ReserveForPrefix; tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_;// this includes reserve + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; @@ -315,10 +223,7 @@ public unsafe void ProcessBytes(in SegmentUnsafe first, in Segment second) offset_ = ReserveForPrefix; tempbuffCnt = 0; - //PrimitiveEncoder.WriteInt32(tempBuff, ref offset_, msgNo); - //tempBuff[offset_++] = (byte)totalNumSeq; - //tempBuff[offset_++] = curresntSeq++; - //tempbuffCnt += offset_; + WriteMetadata((ushort)FragmentSize, msgNo, (byte)totalNumSeq, curresntSeq, tempBuff, ref offset_); curresntSeq++; tempbuffCnt += offset_; diff --git a/NetworkLibrary/UDP/Reliable/Components/ReceiverModule.cs b/NetworkLibrary/UDP/Reliable/Components/ReceiverModule.cs index fa7b01f..638a6c7 100644 --- a/NetworkLibrary/UDP/Reliable/Components/ReceiverModule.cs +++ b/NetworkLibrary/UDP/Reliable/Components/ReceiverModule.cs @@ -156,6 +156,7 @@ private void ConsumerLoop(object state = null) { MiniLogger.Log(MiniLogger.LogLevel.Error, $"Critical errror occured on reliable udp receiver module: {ex.Message} Trace:{ex.StackTrace}"); + throw; } } } diff --git a/NetworkLibrary/UDP/Reliable/Components/ReliableModule.cs b/NetworkLibrary/UDP/Reliable/Components/ReliableModule.cs index 50c9f8c..9ffcf62 100644 --- a/NetworkLibrary/UDP/Reliable/Components/ReliableModule.cs +++ b/NetworkLibrary/UDP/Reliable/Components/ReliableModule.cs @@ -20,6 +20,16 @@ public ReliableModule(IPEndPoint adress) receiver.OnMessageReceived += OutputMessageReceived; } + public ReliableModule(IPEndPoint adress,SenderModule sender) + { + this.sender = sender; + Endpoint = adress; + sender.SendRequested += OutputDataBytesToSend; + + receiver.SendFeedback += OutputFeedbacks; + receiver.OnMessageReceived += OutputMessageReceived; + } + private void OutputMessageReceived(byte[] buffer, int offset, int count) { OnReceived?.Invoke(this, buffer, offset, count); diff --git a/NetworkLibrary/UDP/Reliable/Components/SenderModule.cs b/NetworkLibrary/UDP/Reliable/Components/SenderModule.cs index 1be7116..8d36e19 100644 --- a/NetworkLibrary/UDP/Reliable/Components/SenderModule.cs +++ b/NetworkLibrary/UDP/Reliable/Components/SenderModule.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; namespace NetworkLibrary.UDP.Reliable.Components { @@ -372,6 +373,7 @@ private void StartProducer() { MiniLogger.Log(MiniLogger.LogLevel.Error, $"Error occured on rudp sender execution loop: {ex.Message}"); + throw; } } diff --git a/NetworkLibrary/UDP/Reliable/RudpClient.cs b/NetworkLibrary/UDP/Reliable/RudpClient.cs index 4b6d3a3..554bb69 100644 --- a/NetworkLibrary/UDP/Reliable/RudpClient.cs +++ b/NetworkLibrary/UDP/Reliable/RudpClient.cs @@ -62,7 +62,7 @@ public Task ConnectAsync(IPEndPoint ep) connected.TrySetException(new TimeoutException()); module.Close(); } - }); + }, TaskScheduler.Default); return connected.Task; } diff --git a/NetworkLibrary/Utils/AsyncDispatcher.cs b/NetworkLibrary/Utils/AsyncDispatcher.cs new file mode 100644 index 0000000..8d3a257 --- /dev/null +++ b/NetworkLibrary/Utils/AsyncDispatcher.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkLibrary.Utils +{ + internal class AsyncDispatcher + { + TaskCompletionSource signal = new TaskCompletionSource(); + int cancel = 0; + public void Signal() + { + signal.TrySetResult(true); + } + + public void Abort() + { + Interlocked.Increment(ref cancel); + signal.TrySetResult(true); + } + + public async Task ExecuteBySignalEfficient(Action execution, int delayBetweenMs = 1000) + { + while (true) + { + try + { + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0) return; + await signal.Task.ConfigureAwait(false); + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0)return; + + Interlocked.Exchange(ref signal, new TaskCompletionSource()); + + execution?.Invoke(); + await Task.Delay(delayBetweenMs).ConfigureAwait(false); + } + catch(Exception e) + { + throw new AggregateException(e); + } + } + } + + public async Task ExecuteBySignal(Action execution) + { + while (true) + { + try + { + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0) return; + await signal.Task.ConfigureAwait(false); + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0) return; + + Interlocked.Exchange(ref signal, new TaskCompletionSource()); + + execution?.Invoke(); + } + catch (Exception e) + { + throw new AggregateException(e); + } + } + } + + public async Task LoopPeriodic(Action execution, int ms) + { + Stopwatch sw = new Stopwatch(); + while (true) + { + try + { + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0) return; + + sw.Stop(); + int sleep = ms-(int)sw.ElapsedMilliseconds; + sleep = Math.Max(sleep, 0); + await Task.Delay(ms).ConfigureAwait(false); + + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0) return; + + sw.Restart(); + execution.Invoke(); + } + catch (Exception e) + { + throw new AggregateException(e); + } + + + } + } + //Func + + public async Task LoopPeriodicTask(Func asyncTask, int ms) + { + while (true) + { + try + { + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0) return; + await Task.Delay(ms).ConfigureAwait(false); + + if (Interlocked.CompareExchange(ref cancel, 0, 0) > 0) return; + await asyncTask.Invoke().ConfigureAwait(false); + } + catch (Exception e) + { + throw new AggregateException(e); + } + + + } + } + } +} diff --git a/NetworkLibrary/Utils/Statistics.cs b/NetworkLibrary/Utils/Statistics.cs new file mode 100644 index 0000000..56f4a3c --- /dev/null +++ b/NetworkLibrary/Utils/Statistics.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NetworkLibrary.Utils +{ + public class Statistics + { + public static IEnumerable FilterOutliers(IEnumerable times) + { + if (times == null || times.Count() < 4) + { + throw new ArgumentException("The list must contain at least 4 elements."); + } + + // Sort the list + var sortedTimes = times.OrderBy(t => t).ToList(); + + // Calculate the quartiles + double Q1 = GetPercentile(sortedTimes, 25); + double Q3 = GetPercentile(sortedTimes, 75); + + // Calculate the interquartile range (IQR) + double IQR = Q3 - Q1; + + // Define the acceptable range for non-outliers + double lowerBound = Q1 - 1.5 * IQR; + double upperBound = Q3 + 1.5 * IQR; + + // Filter out the outliers + var filteredTimes = sortedTimes.Where(t => t >= lowerBound && t <= upperBound); + + return filteredTimes; + } + + public static IEnumerable FilterOutliers(List times) + { + if (times == null || times.Count < 4) + { + throw new ArgumentException("The list must contain at least 4 elements."); + } + + // Sort the list + var sortedTimes = times.OrderBy(t => t).ToList(); + + // Calculate the quartiles + TimeSpan Q1 = GetPercentile(sortedTimes, 25); + TimeSpan Q3 = GetPercentile(sortedTimes, 75); + + // Calculate the interquartile range (IQR) + TimeSpan IQR = Q3 - Q1; + + // Define the acceptable range for non-outliers + TimeSpan lowerBound = Q1 - TimeSpan.FromTicks((long)(1.5 * IQR.Ticks)); + TimeSpan upperBound = Q3 + TimeSpan.FromTicks((long)(1.5 * IQR.Ticks)); + + // Filter out the outliers + var filteredTimes = sortedTimes.Where(t => t >= lowerBound && t <= upperBound); + + return filteredTimes; + } + + + private static double GetPercentile(List sortedList, double percentile) + { + int N = sortedList.Count; + double rank = (percentile / 100.0) * (N - 1); + int lowIndex = (int)Math.Floor(rank); + int highIndex = (int)Math.Ceiling(rank); + double fraction = rank - lowIndex; + + if (highIndex >= N) return sortedList[lowIndex]; + return sortedList[lowIndex] + fraction * (sortedList[highIndex] - sortedList[lowIndex]); + } + + private static TimeSpan GetPercentile(List sortedList, double percentile) + { + int N = sortedList.Count; + double rank = (percentile / 100.0) * (N - 1); + int lowIndex = (int)Math.Floor(rank); + int highIndex = (int)Math.Ceiling(rank); + double fraction = rank - lowIndex; + + if (highIndex >= N) return sortedList[lowIndex]; + + // Interpolate between the low and high index TimeSpans + TimeSpan lowValue = sortedList[lowIndex]; + TimeSpan highValue = sortedList[highIndex]; + double ticksDifference = (highValue - lowValue).Ticks; + + return lowValue + TimeSpan.FromTicks((long)(fraction * ticksDifference)); + } + } +} diff --git a/Protobuff/P2P/RelayClient.cs b/Protobuff/P2P/RelayClient.cs index 80d499e..b5d4558 100644 --- a/Protobuff/P2P/RelayClient.cs +++ b/Protobuff/P2P/RelayClient.cs @@ -7,7 +7,7 @@ namespace Protobuff.P2P { public class RelayClient : RelayClientBase - { + { public RelayClient(X509Certificate2 clientCert, int udpPort = 0) : base(clientCert,udpPort) { } diff --git a/Protobuff/Protobuff.csproj b/Protobuff/Protobuff.csproj index aefda6f..318d495 100644 --- a/Protobuff/Protobuff.csproj +++ b/Protobuff/Protobuff.csproj @@ -11,7 +11,7 @@ Apache-2.0 ProtobufNetworkLibrary 2.01 - 3.0.0 + 3.0.1 2.0.1 True diff --git a/Tests/UnitTests/DistributedP2P/DistP2PServerclientTest.cs b/Tests/UnitTests/DistributedP2P/DistP2PServerclientTest.cs new file mode 100644 index 0000000..49d4ff8 --- /dev/null +++ b/Tests/UnitTests/DistributedP2P/DistP2PServerclientTest.cs @@ -0,0 +1,911 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NetworkLibrary; +using NetworkLibrary.DistributedP2P.Channels; +using NetworkLibrary.DistributedP2P.Channels.Components; +using NetworkLibrary.DistributedP2P.Client; +using NetworkLibrary.DistributedP2P.Components; +using NetworkLibrary.DistributedP2P.Server; +using Protobuff.Components.Serialiser; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace UnitTests.DistributedP2P +{ + + class ClientAuthToken : IClientAuthenticationToken + { + public string Token { get; } = "1234"; + + public string AuthenticationMethod { get; } = "1234"; + + public string AdditionalData { get; } = "1234"; + } + class ClientAuth : IClientAuthenticationProvider + { + public IClientAuthenticationToken Authenticate() + { + return new ClientAuthToken(); + } + } + class DbInfo : IClientDbInfo + { + public bool IsValid { get; } = true; + + public string Error { get; } + + public Guid ClientId { get; } = Guid.NewGuid(); + } + class ServerDb : IServerDbConnector + { + public Task GetClientData(IAuthenticationResult result) + { + IClientDbInfo dbInfo = new DbInfo(); + return Task.FromResult(dbInfo); + } + + public Task RegisterClient(IAuthenticationResult tokenResult, byte[] payload) + { + IClientDbInfo dbInfo = new DbInfo(); + return Task.FromResult(dbInfo); + } + } + class AuthResult : IAuthenticationResult + { + public bool IsValid { get; } = true; + + public string UserId { get; } = "1234"; + + public string Error { get; } + + public IReadOnlyDictionary Claims { get; } = new Dictionary(); + } + + class ServerAuth : IAuthenticator + { + public Task Authenticate(string AuthenticationToken, string AuthenticationMethod, string Cookies) + { + IAuthenticationResult result = new AuthResult(); + return Task.FromResult(result); + } + } + + + class ClientDB : IClientDbConnection + { + public byte[] GetClientPublicData() + { + return new byte[80]; + } + } + [TestClass] + public class DistP2PServerclientTest + { + private const string ServerIp = "127.0.0.1"; + //private const string ServerIp = "35.159.121.192"; + + private static DistributedLobbyServerBase ArrangeServer() + { + var dep = new Dependencies() + { + Authenticator = new ServerAuth(), + DbConnector = new ServerDb(), + logger = new Logger() + + }; + dep.logger.LogAvailable += (data) => { Console.WriteLine(dep.logger.Stringify(data)); }; + var param = new ServerParameters() + { + certificate = null, + SSlPort = 20010, + TcpPort = 20011, + UdpPort = 20012, + DiscoveryServerPort = 20013, + + }; + var server = new DistributedLobbyServerBase(dep, param); + server.StartServer(); + return server; + } + + private static DistributedLobbyClient GetClient() + { + var logger = new Logger(); + logger.LogAvailable += (data) => { Console.WriteLine(logger.Stringify(data)); }; + return new DistributedLobbyClient(new ClientDB(), new ClientAuth(), logger); + } + + + [TestMethod] + public void ConnectTest() + { + + + var distributedLobbyClient = GetClient(); + using var server = ArrangeServer(); + var res = distributedLobbyClient.ConnectAsync(ServerIp, 20010).Result; + } + + + [TestMethod] + public void PipeTest() + { + var cl = GetClient(); + var cl2 = GetClient(); + + TaskCompletionSource tcs = new TaskCompletionSource(); + ManualResetEvent mre = new ManualResetEvent(false); + + + using var server = ArrangeServer(); + + + cl2.PeerConnected += PeerConnected; + + var res = cl.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + + var info = new ChannelInfo(); + info.ChannelName = "Test"; + info.ChannelType = ChannelType.Tcp; + var channel1 = (TcpChannel)cl.OpenRelayChannel(cl2.SessionId, info).Result; + + Assert.IsNotNull(channel1); + + byte[] data = new byte[1280000]; + + channel1.Start(); + channel1.Send(data, 0, data.Length); + Thread.Sleep(100); + mre.Set(); + + int received = 0; + + void PeerConnected(IChannel channel_) + { + mre.WaitOne();//emulate bad syncronisation + var channel = (TcpChannel)channel_; + channel.OnBytesReceived += Channel_BytesReceived; + channel.OnDisconnected += Disconnected; + channel.Start(); + } + + void Disconnected() + { + Console.WriteLine("DC"); + tcs.TrySetResult(false); + } + + void Channel_BytesReceived(byte[] buff, int offset, int count) + { + received = count; + tcs.TrySetResult(true); + } + + var ss = tcs.Task.Result; + Assert.IsTrue(ss); + + Assert.AreEqual(received, data.Length); + + } + + [TestMethod] + public void SecurePipeTest() + { + var cl = GetClient(); + var cl2 = GetClient(); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + Task.Delay(24000).ContinueWith(t => + { + tcs.TrySetResult(false); + }); + + ManualResetEvent mre = new ManualResetEvent(false); + var received = new ConcurrentQueue(); + int iter = 20; + + using var server = ArrangeServer(); + + var res = cl.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.PeerConnected += PeerConnected; + + var info = new ChannelInfo(); + info.ChannelName = "Test"; + info.ChannelType = ChannelType.SecureTcp; + var channel1 = (SecureTcpChannel)cl.OpenRelayChannel(cl2.SessionId, info).Result; + + Assert.IsNotNull(channel1); + + byte[] data = new byte[1280000]; + + channel1.Start(); + + + var ping = channel1.Ping().Result; + + for (int i = 0; i < iter; i++) + { + data[0] = (byte)i; + channel1.Send(data, 0, data.Length); + if (i % 2 == 0) + Thread.Sleep(1000); + } + + + void PeerConnected(IChannel channel_) + { + // mre.WaitOne();//emulate bad syncronisation + var channel = (SecureTcpChannel)channel_; + channel.OnBytesReceived += Channel_BytesReceived; + channel.OnDisconnected += Disconnected; + channel.Start(); + } + + void Disconnected() + { + Console.WriteLine("DC"); + tcs.TrySetResult(false); + } + + void Channel_BytesReceived(byte[] buff, int offset, int count) + { + received.Enqueue(buff[offset]); + + if (received.Count == iter) + tcs.TrySetResult(true); + } + + + + + var ss = tcs.Task.Result; + Assert.IsTrue(received.Count == iter); + + var rec = received.ToList(); + for (int i = 0; i < received.Count; i++) + { + Assert.IsTrue(rec[i] == i); + } + + } + + [TestMethod] + public void PipeTestUdp() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + + int received = 0; + + using var server = ArrangeServer(); + var cl1 = GetClient(); + var cl2 = GetClient(); + + var res = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelName = "Test"; + info.ChannelType = ChannelType.Udp; + var channel1 = (UdpChannel)cl1.OpenRelayChannel(cl2.SessionId, info).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + byte[] data = new byte[12800]; + channel1.Send(data, 0, data.Length); + Thread.Sleep(100); + + void Cl2_PeerConnected(IChannel obj) + { + var udpChannel = (UdpChannel)obj; + udpChannel.OnBytesReceived += (b, o, c) => + { + received = c; + tcs.SetResult(true); + }; + udpChannel.Start(); + } + + var ss = tcs.Task.Result; + Assert.AreEqual(received, data.Length); + + } + + [TestMethod] + public void PipeTestUdpSecure() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + + int received = 0; + int cnt = 0; + + using var server = ArrangeServer(); + var cl1 = GetClient(); + var cl2 = GetClient(); + + var res = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelName = "Test"; + info.ChannelType = ChannelType.SecureUdp; + var channel1 = (SecureUdpChannel)cl1.OpenRelayChannel(cl2.SessionId, info).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + + Thread.Sleep(5000); + var ping = channel1.Ping().Result; + Console.WriteLine(ping); + + byte[] data = new byte[1280000]; + channel1.SendReliable(data, 0, data.Length); + Thread.Sleep(5000); + channel1.SendReliable(data, 0, data.Length); + Thread.Sleep(100); + + void Cl2_PeerConnected(IChannel obj) + { + var udpChannel = (SecureUdpChannel)obj; + udpChannel.OnBytesReceived += (b, o, c) => + { + received = c; + if (++cnt == 2) + { + tcs.SetResult(true); + + } + }; + udpChannel.Start(); + } + + var ss = tcs.Task.Result; + Assert.AreEqual(received, data.Length); + + Thread.Sleep(1000); + + } + + + [TestMethod] + public void MessageTest() + { + var cl = GetClient(); + var cl2 = GetClient(); + + TaskCompletionSource tcs = new TaskCompletionSource(); + int received = 0; + + using var server = ArrangeServer(); + + var res = cl.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.MessageReceived += MsgRec; + MessageEnvelope msg = new MessageEnvelope(); + msg.Header = "Greetings"; + msg.Payload = new byte[1280000]; + msg.To = cl2.SessionId; + cl.SendAsyncMessage(msg); + + void MsgRec(NetworkLibrary.MessageEnvelope msg) + { + tcs.SetResult(true); + received = msg.PayloadCount; + } + + var ss = tcs.Task.Result; + Assert.IsTrue(ss); + + Assert.AreEqual(received, msg.Payload.Length); + + + } + + + [TestMethod] + public void StatusPublishTest() + { + using var server = ArrangeServer(); + List> clients = new List>(); + HashSet ids = new HashSet(); + + List pending = new List(); + for (int i = 0; i < 20; i++) + { + var cl = GetClient(); + Task task = cl.ConnectAsync(ServerIp, 20010).ContinueWith(t => + { + clients.Add(cl); + ids.Add(cl.SessionId); + }); + task.ConfigureAwait(false); + + pending.Add(task); + + } + + Task.WhenAll(pending).Wait(); + pending.Clear(); + + Thread.Sleep(2000); + VerifyPeerList(clients, ids); + + for (int i = 0; i < 5; i++)//dc 5 + { + ids.Remove(clients[i].SessionId); + clients[i].Disconnect(); + clients.RemoveAt(i); + } + + Thread.Sleep(2000); + VerifyPeerList(clients, ids); + + for (int i = 0; i < 15; i++) //+ 15 + { + var cl = GetClient(); + Task task = cl.ConnectAsync(ServerIp, 20010).ContinueWith(t => + { + clients.Add(cl); + ids.Add(cl.SessionId); + }); + task.ConfigureAwait(false); + + pending.Add(task); + + } + + Task.WhenAll(pending).Wait(); + pending.Clear(); + + Thread.Sleep(2000); + VerifyPeerList(clients, ids); + + + for (int i = 0; i < 15; i++) //+- 15 + { + var cl = GetClient(); + var res = cl.ConnectAsync(ServerIp, 20010).Result; + clients.Add(cl); + ids.Add(cl.SessionId); + + ids.Remove(clients[i].SessionId); + clients[i].Disconnect(); + clients.RemoveAt(i); + + } + + Thread.Sleep(2000); + VerifyPeerList(clients, ids); + } + + private static void VerifyPeerList(List> clients, HashSet ids) + { + foreach (var client in clients) + { + var pl = client.GetPeerList(); + Assert.IsNotNull(pl); + Assert.IsTrue(pl.Count == clients.Count - 1); + + foreach (var peerKv in pl) + { + Assert.IsTrue(ids.Contains(peerKv.Key)); + Assert.IsTrue(peerKv.Key != client.SessionId); + } + } + } + + [TestMethod] + public void SendAndWait() + { + using var server = ArrangeServer(); + var cl1 = GetClient(); + var cl2 = GetClient(); + + var res = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + + + cl1.MessageReceived += Cl1_MessageReceived; + void Cl1_MessageReceived(MessageEnvelope obj) + { + obj.To = cl2.SessionId; + cl1.SendAsyncMessage(obj); + } + + var msg = new MessageEnvelope(); + msg.Header = "Hello"; + msg.To = cl1.SessionId; + var response = cl2.SendMessageAndWaitResponse(msg).Result; + + Assert.IsTrue(response.Header != MessageEnvelope.RequestTimeout); + } + + [TestMethod] + public void Timesync() + { + + using var server = ArrangeServer(); + Thread.Sleep(1337); + var cl1 = GetClient(); + Thread.Sleep(1337); + var cl2 = GetClient(); + + var res = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + double time1 = cl1.GetTime(); + double time2 = cl2.GetTime(); + double time3 = server.GetTime(); + + + Console.WriteLine(time1); + Console.WriteLine(time2); + Console.WriteLine(time3); + + Assert.IsTrue(Math.Abs(time1 - time2) < 1); + Assert.IsTrue(Math.Abs(time1 - time3) < 1); + } + + [TestMethod] + public void UdpHolepunch() + { + + TaskCompletionSource tcs = new TaskCompletionSource(); + ManualResetEvent mre = new ManualResetEvent(false); + int received = 0; + + var cl1 = GetClient(); + var cl2 = GetClient(); + using var server = ArrangeServer(); + + var res1 = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelType = ChannelType.Udp; + info.ChannelName = "Test"; + var channel1 = (UdpChannel)cl1.TryHolePunch(cl2.SessionId, info).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + var data = new byte[12800000]; + data[0] = 1; + channel1.SendReliable(data, 0, data.Length); + Thread.Sleep(100); + + void Cl2_PeerConnected(IChannel obj) + { + var ch = (UdpChannel)obj; + ch.OnBytesReceived += Ch_OnMessageReceived; + ch.Start(); + } + + void Ch_OnMessageReceived(byte[] arg1, int arg2, int arg3) + { + received = arg3; + tcs.TrySetResult(true); + } + + var ss = tcs.Task.Result; + Assert.AreEqual(data.Length, received); + + + + } + + [TestMethod] + public void UdpHolepunchSecure() + { + + TaskCompletionSource tcs = new TaskCompletionSource(); + ManualResetEvent mre = new ManualResetEvent(false); + int received = 0; + + var cl1 = GetClient(); + var cl2 = GetClient(); + using var server = ArrangeServer(); + + var res1 = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelType = ChannelType.SecureUdp; + info.ChannelName = "Test"; + var channel1 = (SecureUdpChannel)cl1.TryHolePunch(cl2.SessionId, info).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + var data = new byte[1280000]; + data[0] = 1; + channel1.SendReliable(data, 0, data.Length); + Thread.Sleep(100); + + void Cl2_PeerConnected(IChannel obj) + { + var ch = (SecureUdpChannel)obj; + ch.OnBytesReceived += Ch_OnMessageReceived; + ch.Start(); + } + + void Ch_OnMessageReceived(byte[] arg1, int arg2, int arg3) + { + received = arg3; + tcs.TrySetResult(true); + } + + var ss = tcs.Task.Result; + Assert.AreEqual(data.Length, received); + + } + + [TestMethod] + public void TcpHolepunch() + { + + TaskCompletionSource tcs = new TaskCompletionSource(); + ManualResetEvent mre = new ManualResetEvent(false); + int received = 0; + + var cl1 = GetClient(); + var cl2 = GetClient(); + using var server = ArrangeServer(); + + var res1 = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelType = ChannelType.Tcp; + info.ChannelName = "Test"; + var channel1 = (TcpChannel)cl1.TryHolePunch(cl2.SessionId, info, TcpHolePunchStrategy.Sequential).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + var data = new byte[1280]; + data[0] = 1; + channel1.Send(data, 0, data.Length); + Thread.Sleep(100); + + void Cl2_PeerConnected(IChannel obj) + { + var ch = (TcpChannel)obj; + ch.OnBytesReceived += Ch_OnMessageReceived; + ch.Start(); + } + + void Ch_OnMessageReceived(byte[] arg1, int arg2, int arg3) + { + received = arg3; + tcs.TrySetResult(true); + } + + var ss = tcs.Task.Result; + Assert.AreEqual(data.Length, received); + + + + } + + [TestMethod] + public void TcpHolepunchSecure() + { + + TaskCompletionSource tcs = new TaskCompletionSource(); + ManualResetEvent mre = new ManualResetEvent(false); + int received = 0; + + var cl1 = GetClient(); + var cl2 = GetClient(); + using var server = ArrangeServer(); + + var res1 = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelType = ChannelType.SecureTcp; + info.ChannelName = "Test"; + var channel1 = (SecureTcpChannel)cl1.TryHolePunch(cl2.SessionId, info).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + var data = new byte[12800000]; + data[0] = 1; + channel1.Send(data, 0, data.Length); + Thread.Sleep(100); + + void Cl2_PeerConnected(IChannel obj) + { + var ch = (SecureTcpChannel)obj; + ch.OnBytesReceived += Ch_OnMessageReceived; + ch.Start(); + } + + void Ch_OnMessageReceived(byte[] arg1, int arg2, int arg3) + { + received = arg3; + tcs.TrySetResult(true); + } + + var ss = tcs.Task.Result; + Assert.AreEqual(data.Length, received); + + + + } + + [TestMethod] + public void TestTcpChannelParallelSend() + { + + TaskCompletionSource tcs = new TaskCompletionSource(); + ManualResetEvent mre = new ManualResetEvent(false); + int numReceived = 0; + + var cl1 = GetClient(); + var cl2 = GetClient(); + using var server = ArrangeServer(); + + var res1 = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + var data = new byte[1280]; + data[0] = 1; + int iter = 1000; + + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelType = ChannelType.Tcp; + info.ChannelName = "Test"; + var channel1 = (TcpChannel)cl1.TryHolePunch(cl2.SessionId, info, TcpHolePunchStrategy.Sequential).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + Thread.Sleep(100); + + Parallel.For(0, iter, (i) => + { + channel1.Send(data, 0, data.Length); + }); + + void Cl2_PeerConnected(IChannel obj) + { + var ch = (TcpChannel)obj; + ch.OnBytesReceived += Ch_OnMessageReceived; + ch.Start(); + } + + void Ch_OnMessageReceived(byte[] arg1, int arg2, int arg3) + { + if (arg3 != data.Length) + throw new Exception(); + + if (Interlocked.Increment(ref numReceived) == iter) + tcs.TrySetResult(true); + } + + var ss = tcs.Task.Result; + Assert.AreEqual(iter, numReceived); + + + + } + + [TestMethod] + public void TestTcpChannelOrder() + { + + TaskCompletionSource tcs = new TaskCompletionSource(); + ManualResetEvent mre = new ManualResetEvent(false); + int numReceived = 0; + + var cl1 = GetClient(); + var cl2 = GetClient(); + using var server = ArrangeServer(); + + var res1 = cl1.ConnectAsync(ServerIp, 20010).Result; + var res2 = cl2.ConnectAsync(ServerIp, 20010).Result; + + var data = new byte[1280]; + data[0] = 1; + int iter = 100; + List values = new List(); + + + cl2.PeerConnected += Cl2_PeerConnected; + + var info = new ChannelInfo(); + info.ChannelType = ChannelType.Tcp; + info.ChannelName = "Test"; + var channel1 = (TcpChannel)cl1.TryHolePunch(cl2.SessionId, info, TcpHolePunchStrategy.Sequential).Result; + Assert.IsNotNull(channel1); + channel1.Start(); + + Thread.Sleep(100); + + for (int i = 0; i < iter; i++) + { + data[0] = (byte)i; + channel1.Send(data, 0, data.Length); + if (i % 10 == 0) + Thread.Sleep(1); + }; + + void Cl2_PeerConnected(IChannel obj) + { + var ch = (TcpChannel)obj; + ch.OnBytesReceived += Ch_OnMessageReceived; + ch.Start(); + } + void Ch_OnMessageReceived(byte[] arg1, int arg2, int arg3) + { + if (arg3 != data.Length) + throw new Exception(); + + values.Add(arg1[arg2]); + + if (Interlocked.Increment(ref numReceived) == 100) + tcs.TrySetResult(true); + } + Task.Delay(5000).ContinueWith(t => + { + tcs.TrySetResult(false); + }); + var ss = tcs.Task.Result; + Assert.AreEqual(iter, numReceived); + + for (int i = 1; i < values.Count; i++) + { + Assert.IsTrue(values[i - 1] + 1 == values[i]); + } + + } + + [TestMethod] + public void NTPClientServer() + { + Stopwatch serverClock = Stopwatch.StartNew(); + using NTPServer server = new NTPServer(22222, serverClock); + server.Start(); + Thread.Sleep(1000); + Stopwatch clientClock = Stopwatch.StartNew(); + NTPClient cl = new NTPClient("127.0.0.1", 22222, clientClock); + + Parallel.For(0, 100, i => + { + _ = cl.GetServerTime(2000); + }); + + + var timeResult = cl.GetServerTime(2000).Result; + + Assert.IsTrue(timeResult.Succes); + Assert.IsTrue(timeResult.PreciseTime > 1000); + + } + + + } +} diff --git a/Tests/UnitTests/LobbyServerTest.cs b/Tests/UnitTests/LobbyServerTest.cs index 96d3552..c06012a 100644 --- a/Tests/UnitTests/LobbyServerTest.cs +++ b/Tests/UnitTests/LobbyServerTest.cs @@ -6,9 +6,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading; +using System.Threading.Tasks; namespace UnitTests { @@ -89,6 +91,219 @@ void TcpReceived(SecureProtoRoomClient cl, MessageEnvelope m) server.ShutdownServer(); } + [TestMethod] + public void TestBatchRoomBroadcast() + { + var expectedClientsTcp = new ConcurrentDictionary(); + var expectedClientsUdp = new ConcurrentDictionary(); + + int totalUdp = 0; + int totalTcp = 0; + + var scert = new X509Certificate2("server.pfx", "greenpass"); + var cert = new X509Certificate2("client.pfx", "greenpass"); + string ip = "127.0.0.1"; + int port = 22222; + int numClients = 10; + int numMsgs = 1000; + var server = new RoomServer(port, scert); + server.StartServer(); + + List clients = new List(); + + for (int i = 0; i < numClients; i++) + { + var cl = new SecureProtoRoomClient(cert); + cl.OnTcpMessageReceived += (m) => TcpReceived(cl, m); + cl.OnUdpMessageReceived += (m) => UdpReceived(cl, m); + cl.Connect(ip, port); + cl.CreateOrJoinRoom("WA"); + clients.Add(cl); + } + + Thread.Sleep(1000); + for (int j = 0; j < numMsgs; j++) + { + for (int i = 0; i < numClients; i++) + { + clients[i].BroadcastMessageToRoomBatched("WA", new MessageEnvelope() { Header = "Tcp Yo" }); + } + } + + + + + + void UdpReceived(SecureProtoRoomClient cl, MessageEnvelope m) + { + expectedClientsUdp.TryAdd(cl, null); + Interlocked.Increment(ref totalUdp); + } + + void TcpReceived(SecureProtoRoomClient cl, MessageEnvelope m) + { + expectedClientsTcp.TryAdd(cl, null); + Interlocked.Increment(ref totalTcp); + + } + Thread.Sleep(2000); + Assert.AreEqual(clients.Count, expectedClientsTcp.Count); + Assert.AreEqual(totalTcp, numClients*numMsgs*(clients.Count - 1)); + + server.ShutdownServer(); + } + + [TestMethod] + public void TestTimeSync() + { + var expectedClientsTcp = new ConcurrentDictionary(); + var expectedClientsUdp = new ConcurrentDictionary(); + + int totalUdp = 0; + int totalTcp = 0; + + var scert = new X509Certificate2("server.pfx", "greenpass"); + var cert = new X509Certificate2("client.pfx", "greenpass"); + string ip = "127.0.0.1"; + int port = 22222; + int numClients = 10; + var server = new RoomServer(port, scert); + server.StartServer(); + Thread.Sleep(1000); + + List clients = new List(); + + for (int i = 0; i < numClients; i++) + { + var cl = new SecureProtoRoomClient(cert); + + cl.Connect(ip, port); + cl.CreateOrJoinRoom("WA"); + clients.Add(cl); + Thread.Sleep(1); + } + + Thread.Sleep(1000); + + for (int j = 0; j < 10; j++) + { + for (int i = 0; i < numClients; i++) + { + var suc = clients[i].SyncTime().Result; + } + Thread.Sleep(100); + } + + + + + Thread.Sleep(1000); + + var t1 = clients[0].GetTime(); + var tn = clients[numClients-1].GetTime(); + + Trace.WriteLine(t1); + Trace.WriteLine(tn); + Assert.IsTrue(Math.Abs(t1-tn)<1); + + + + + server.ShutdownServer(); + } + [TestMethod] + public void TestTimeSync2() + { + var expectedClientsTcp = new ConcurrentDictionary(); + var expectedClientsUdp = new ConcurrentDictionary(); + + int totalUdp = 0; + int totalTcp = 0; + + var scert = new X509Certificate2("server.pfx", "greenpass"); + var cert = new X509Certificate2("client.pfx", "greenpass"); + string ip = "127.0.0.1"; + int port = 22222; + int numClients = 10; + var server = new RoomServer(port, scert); + server.StartServer(); + Thread.Sleep(1000); + + List clients = new List(); + + for (int i = 0; i < numClients; i++) + { + var cl = new SecureProtoRoomClient(cert); + cl.StartAutoTimeSync(1000, false); + cl.Connect(ip, port); + + cl.CreateOrJoinRoom("MA"); + clients.Add(cl); + Thread.Sleep(1); + } + + Thread.Sleep(2400); + + + var t1 = clients[0].GetTime(); + var tn = clients[numClients - 1].GetTime(); + + Trace.WriteLine(t1); + Trace.WriteLine(tn); + Assert.IsTrue(Math.Abs(t1 - tn) < 1); + + server.ShutdownServer(); + } + + [TestMethod] + public void TestRoomList() + { + var expectedClientsTcp = new ConcurrentDictionary(); + var expectedClientsUdp = new ConcurrentDictionary(); + + int totalUdp = 0; + int totalTcp = 0; + + var scert = new X509Certificate2("server.pfx", "greenpass"); + var cert = new X509Certificate2("client.pfx", "greenpass"); + string ip = "127.0.0.1"; + int port = 22222; + int numClients = 10; + var server = new RoomServer(port, scert); + server.StartServer(); + Thread.Sleep(1000); + + ConcurrentDictionary clients = new ConcurrentDictionary(); + + Parallel.For(0, numClients, i => + { + var cl = new SecureProtoRoomClient(cert); + + cl.Connect(ip, port); + clients.TryAdd(cl.SessionId, cl); + + var list = cl.CreateOrJoinRoom("WA"); + + Assert.IsTrue(list != null); + Assert.IsTrue(list.Peers != null); + Assert.IsTrue(list.Peers.PeerIds != null); + Assert.IsTrue(list.Peers.PeerIds.Count > 0); + + foreach (var kv in list.Peers.PeerIds) + { + Assert.IsTrue(clients.ContainsKey(kv.Key)); + } + + + }); + + Thread.Sleep(1000); + + + + server.ShutdownServer(); + } + [TestMethod] public void TestRoomBroadcastWithLeave() {