diff --git a/README.md b/README.md index 6060a6c3..0e99a461 100644 --- a/README.md +++ b/README.md @@ -31,36 +31,41 @@ dotnet test ## Roadmap -From the beginning, the target is to provide a performant well-tested implementation that works on multiple platforms, with high throughput and low memory profile. -See the [milestones](https://github.com/NethermindEth/dotnet-libp2p/milestones?direction=asc&sort=due_date&state=open). +🚧 The library is not stable and under heavy development. Consider the [beta](https://github.com/NethermindEth/dotnet-libp2p/milestone/5) milestone as a reflection of readiness for production 🚧 + +The target is to provide a performant well-tested implementation of a wide range of protocols that works on multiple platforms, with high throughput and low memory profile. + | Protocol | Version | Status | |--------------------|--------------------|-----------------| -| **Transports** | TCP | tcp | ✅ | -| QUIC | quic | 🚧 | -| | quic-v1 | 🚧 | -| **Protocols** +| QUIC | quic-v1 | ✅ | | multistream-select | /multistream/1.0.0 | ✅ | | plaintext | /plaintext/2.0.0 | ✅ | | noise | /noise | ✅ | +| tls | /tls/1.0.0 | ⬜ help wanted | +| WebTransport | | ⬜ help wanted | | yamux | /yamux/1.0.0 | ✅ | +| Circuit Relay | /libp2p/circuit/relay/0.2.0/* | ⬜ help wanted | +| hole punching | | ⬜ help wanted | +| **Application layer** | Identify | /ipfs/id/1.0.0 | ✅ | | ping | /ipfs/ping/1.0.0 | ✅ | | pubsub | /floodsub/1.0.0 | ✅ | | | /meshsub/1.0.0 | ✅ | | | /meshsub/1.1.0 | 🚧 | | | /meshsub/1.2.0 | ⬜ | -| Circuit Relay | /libp2p/circuit/relay/0.2.0/* | ⬜ help wanted | | **Discovery** | mDns | basic | ✅ | | | DNS-SD | 🚧 | -| [discv5](https://github.com/Pier-Two/Lantern.Discv5) | 5.1 | ⬜ help wanted | +| [discv5](https://github.com/Pier-Two/Lantern.Discv5) | 5.1 | 🚧 help wanted | ⬜ - not yet implemented
🚧 - work in progress
✅ - basic support implemented +No plans for: mplex, quic(draft-29) + ## License dotnet-libp2p is an open-source software licensed under the [MIT](https://github.com/nethermindeth/dotnet-libp2p/blob/main/LICENSE). diff --git a/docs/development/transport-layer.md b/docs/development/transport-layer.md index 07975763..4dfced2b 100644 --- a/docs/development/transport-layer.md +++ b/docs/development/transport-layer.md @@ -21,7 +21,7 @@ Each protocol should implement `IProtocol` interface, which enforces the `Listen ```csharp namespace Nethermind.Libp2p.Core; -public abstract class MyCustomProtocol +public class MyCustomProtocol : IProtocol { public Task DialAsync(IChannel downChannel, IChannelFactory upChannelFactory, IPeerContext context) { diff --git a/src/libp2p/Directory.Packages.props b/src/libp2p/Directory.Packages.props index 932aec45..48268273 100644 --- a/src/libp2p/Directory.Packages.props +++ b/src/libp2p/Directory.Packages.props @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs b/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs index a1e3f2f7..0060940b 100644 --- a/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs +++ b/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs @@ -3,17 +3,18 @@ using Google.Protobuf; using Nethermind.Libp2p.Core.Dto; + namespace Nethermind.Libp2p.Core.Tests; public class IdentityTests { - [TestCaseSource(nameof(ExpectedKeysEncoded))] + [TestCaseSource(nameof(KeysEncoded))] public void Test_KeyEncoding(byte[] privateKey, byte[] publicKey) { Identity identity = new(PrivateKey.Parser.ParseFrom(privateKey)); Assert.That(identity.PublicKey.ToByteArray(), Is.EquivalentTo(publicKey)); } - public static IEnumerable ExpectedKeysEncoded() + public static IEnumerable KeysEncoded() { yield return new TestCaseData( Convert.FromHexString("08031279307702010104203E5B1FE9712E6C314942A750BD67485DE3C1EFE85B1BFB520AE8F9AE3DFA4A4CA00A06082A8648CE3D030107A14403420004DE3D300FA36AE0E8F5D530899D83ABAB44ABF3161F162A4BC901D8E6ECDA020E8B6D5F8DA30525E71D6851510C098E5C47C646A597FB4DCEC034E9F77C409E62"), @@ -36,4 +37,22 @@ public static IEnumerable ExpectedKeysEncoded() ) { TestName = "Rsa" }; } + + [TestCaseSource(nameof(KeyTypes))] + public void Test_KeyGeneration(KeyType keyType) + => _ = new Identity(keyType: keyType); + + public static IEnumerable KeyTypes() + => Enum.GetValues().Select(kt => new TestCaseData(kt) { TestName = kt.ToString() }); + + [TestCaseSource(nameof(KeyTypes))] + public void Test_Signing(KeyType keyType) + { + Identity id = new(keyType: keyType); + byte[] message = Enumerable.Range(0, 100).Select(i => (byte)i).ToArray(); + + byte[] signature = id.Sign(message); + + Assert.That(id.VerifySignature(message, signature), Is.True); + } } diff --git a/src/libp2p/Libp2p.Core.TestsBase/TestPeers.cs b/src/libp2p/Libp2p.Core.TestsBase/TestPeers.cs index 01ccc8af..4bb9d81f 100644 --- a/src/libp2p/Libp2p.Core.TestsBase/TestPeers.cs +++ b/src/libp2p/Libp2p.Core.TestsBase/TestPeers.cs @@ -22,6 +22,6 @@ public static Multiaddr Multiaddr(int i) => testPeerAddrs.GetOrAdd(i, i => public static PeerId PeerId(Multiaddr addr) => new(addr.At(Enums.Multiaddr.P2p)!); - public static Identity Identity(Multiaddr addr) => new Identity(Core.PeerId.ExtractPublicKey(PeerId(addr).Bytes)); + public static Identity Identity(Multiaddr addr) => new(Core.PeerId.ExtractPublicKey(PeerId(addr).Bytes)); } diff --git a/src/libp2p/Libp2p.Core/Identity.cs b/src/libp2p/Libp2p.Core/Identity.cs index 8bc07e1e..1c0d4c94 100644 --- a/src/libp2p/Libp2p.Core/Identity.cs +++ b/src/libp2p/Libp2p.Core/Identity.cs @@ -10,6 +10,10 @@ using System.Security.Cryptography; using System.Buffers; using Org.BouncyCastle.Crypto.Parameters; +using ECPoint = Org.BouncyCastle.Math.EC.ECPoint; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.EC; +using Org.BouncyCastle.Crypto.Generators; namespace Nethermind.Libp2p.Core; @@ -18,28 +22,79 @@ namespace Nethermind.Libp2p.Core; /// public class Identity { + private const KeyType DefaultKeyType = KeyType.Ed25519; + public PublicKey PublicKey { get; } public PrivateKey? PrivateKey { get; } - public Identity(byte[]? privateKey = default, KeyType keyType = KeyType.Ed25519) - : this(privateKey is null ? null : new PrivateKey { Data = ByteString.CopyFrom(privateKey), Type = keyType }) + public Identity(byte[]? privateKey = default, KeyType keyType = DefaultKeyType) { + if (privateKey is null) + { + (PrivateKey, PublicKey) = GeneratePrivateKeyPair(keyType); + } + else + { + PrivateKey = new PrivateKey { Data = ByteString.CopyFrom(privateKey), Type = keyType }; + PublicKey = GetPublicKey(PrivateKey); + } } - public Identity(PrivateKey? privateKey) + public Identity(PrivateKey privateKey) { - if (privateKey is null) + PrivateKey = privateKey; + PublicKey = GetPublicKey(PrivateKey); + } + + private (PrivateKey, PublicKey) GeneratePrivateKeyPair(KeyType type) + { + ByteString privateKeyData; + ByteString? publicKeyData = null; + switch (type) { - byte[] rented = ArrayPool.Shared.Rent(Ed25519.SecretKeySize); - Span privateKeyBytesSpan = rented.AsSpan(0, Ed25519.SecretKeySize); - SecureRandom rnd = new(); - Ed25519.GeneratePrivateKey(rnd, privateKeyBytesSpan); - ArrayPool.Shared.Return(rented, true); - privateKey = new PrivateKey { Data = ByteString.CopyFrom(privateKeyBytesSpan), Type = KeyType.Ed25519 }; + case KeyType.Ed25519: + { + Span privateKeyBytes = stackalloc byte[Ed25519.SecretKeySize]; + SecureRandom rnd = new(); + Ed25519.GeneratePrivateKey(rnd, privateKeyBytes); + privateKeyData = ByteString.CopyFrom(privateKeyBytes); + } + break; + case KeyType.Rsa: + { + using RSA rsa = RSA.Create(1024); + privateKeyData = ByteString.CopyFrom(rsa.ExportRSAPrivateKey()); + } + break; + case KeyType.Secp256K1: + { + X9ECParameters curve = ECNamedCurveTable.GetByName("secp256k1"); + ECDomainParameters domainParams = new(curve); + + SecureRandom secureRandom = new(); + ECKeyGenerationParameters keyParams = new(domainParams, secureRandom); + + ECKeyPairGenerator generator = new("ECDSA"); + generator.Init(keyParams); + AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair(); + Span privateKeySpan = stackalloc byte[32]; + ((ECPrivateKeyParameters)keyPair.Private).D.ToByteArrayUnsigned(privateKeySpan); + privateKeyData = ByteString.CopyFrom(privateKeySpan); + publicKeyData = ByteString.CopyFrom(((ECPublicKeyParameters)keyPair.Public).Q.GetEncoded(true)); + } + break; + case KeyType.Ecdsa: + { + using ECDsa rsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + privateKeyData = ByteString.CopyFrom(rsa.ExportECPrivateKey()); + } + break; + default: + throw new NotImplementedException($"{type} generation is not supported"); } - PrivateKey = privateKey; - PublicKey = GetPublicKey(privateKey); + PrivateKey privateKey = new() { Type = type, Data = privateKeyData }; + return (privateKey, publicKeyData is not null ? new PublicKey { Type = type, Data = publicKeyData } : GetPublicKey(privateKey)); } public Identity(PublicKey publicKey) @@ -64,7 +119,7 @@ private static PublicKey GetPublicKey(PrivateKey privateKey) case KeyType.Rsa: { - RSA rsa = RSA.Create(); + using RSA rsa = RSA.Create(); rsa.ImportRSAPrivateKey(privateKey.Data.Span, out int bytesRead); publicKeyData = ByteString.CopyFrom(rsa.ExportSubjectPublicKeyInfo()); } @@ -72,18 +127,17 @@ private static PublicKey GetPublicKey(PrivateKey privateKey) case KeyType.Secp256K1: { - X9ECParameters curve = ECNamedCurveTable.GetByName("secp256k1"); - Org.BouncyCastle.Math.EC.ECPoint pointQ - = curve.G.Multiply(new BigInteger(1, privateKey.Data.Span)); + X9ECParameters curve = CustomNamedCurves.GetByName("secp256k1"); + ECPoint pointQ = curve.G.Multiply(new BigInteger(privateKey.Data.ToArray())); publicKeyData = ByteString.CopyFrom(pointQ.GetEncoded(true)); } break; case KeyType.Ecdsa: { - ECDsa rsa = ECDsa.Create(); - rsa.ImportECPrivateKey(privateKey.Data.Span, out int _); - publicKeyData = ByteString.CopyFrom(rsa.ExportSubjectPublicKeyInfo()); + using ECDsa ecdsa = ECDsa.Create(); + ecdsa.ImportECPrivateKey(privateKey.Data.Span, out int _); + publicKeyData = ByteString.CopyFrom(ecdsa.ExportSubjectPublicKeyInfo()); } break; default: @@ -93,5 +147,99 @@ Org.BouncyCastle.Math.EC.ECPoint pointQ return new() { Type = privateKey.Type, Data = publicKeyData }; } + public bool VerifySignature(byte[] message, byte[] signature) + { + if (PublicKey is null) + { + throw new ArgumentNullException(nameof(PublicKey)); + } + + switch (PublicKey.Type) + { + case KeyType.Ed25519: + { + return Ed25519.Verify(signature, 0, PublicKey.Data.ToByteArray(), 0, message, 0, message.Length); + } + case KeyType.Rsa: + { + using RSA rsa = RSA.Create(); + rsa.ImportSubjectPublicKeyInfo(PublicKey.Data.Span, out _); + return rsa.VerifyData(message, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + case KeyType.Secp256K1: + { + X9ECParameters curve = CustomNamedCurves.GetByName("secp256k1"); + ISigner signer = SignerUtilities.GetSigner("SHA-256withECDSA"); + + ECPublicKeyParameters publicKeyParamters = new( + "ECDSA", + curve.Curve.DecodePoint(PublicKey.Data.ToArray()), + new ECDomainParameters(curve) + ); + + signer.Init(false, publicKeyParamters); + signer.BlockUpdate(message, 0, message.Length); + return signer.VerifySignature(signature); + } + case KeyType.Ecdsa: + { + using ECDsa ecdsa = ECDsa.Create(); + ecdsa.ImportSubjectPublicKeyInfo(PublicKey.Data.Span, out _); + return ecdsa.VerifyData(message, signature, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence); + } + default: + throw new NotImplementedException($"{PublicKey.Type} is not supported"); + } + } + + public byte[] Sign(byte[] message) + { + if (PrivateKey is null) + { + throw new ArgumentException(nameof(PrivateKey)); + } + + switch (PublicKey.Type) + { + case KeyType.Ed25519: + { + byte[] sig = new byte[Ed25519.SignatureSize]; + Ed25519.Sign(PrivateKey.Data.ToByteArray(), 0, PublicKey.Data.ToByteArray(), 0, + message, 0, message.Length, sig, 0); + return sig; + } + case KeyType.Ecdsa: + { + ECDsa e = ECDsa.Create(); + e.ImportECPrivateKey(PrivateKey.Data.Span, out _); + return e.SignData(message, HashAlgorithmName.SHA256, + DSASignatureFormat.Rfc3279DerSequence); + } + case KeyType.Rsa: + { + using RSA rsa = RSA.Create(); + rsa.ImportRSAPrivateKey(PrivateKey.Data.Span, out _); + return rsa.SignData(message, 0, message.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + case KeyType.Secp256K1: + { + X9ECParameters curve = CustomNamedCurves.GetByName("secp256k1"); + ISigner signer = SignerUtilities.GetSigner("SHA-256withECDSA"); + + ECPrivateKeyParameters privateKeyParams = new( + "ECDSA", + new BigInteger(1, PrivateKey.Data.ToArray()), + new ECDomainParameters(curve) + ); + + signer.Init(true, privateKeyParams); + signer.BlockUpdate(message, 0, message.Length); + return signer.GenerateSignature(); + } + default: + throw new NotImplementedException($"{PublicKey.Type} is not supported"); + } + } + public PeerId PeerId => new(PublicKey); } diff --git a/src/libp2p/Libp2p.Core/Multiaddr.cs b/src/libp2p/Libp2p.Core/Multiaddr.cs index 5b5cec3b..f6d6ac79 100644 --- a/src/libp2p/Libp2p.Core/Multiaddr.cs +++ b/src/libp2p/Libp2p.Core/Multiaddr.cs @@ -47,8 +47,8 @@ public override string ToString() return string.Join("", _segments .Select(s => s.Parameter is null - ? new object[] { $"/{s.Type.ToString().ToLower()}" } - : new object[] { $"/{s.Type.ToString().ToLower()}", $"/{s.Parameter}" }) + ? new object[] { $"/{ToString(s.Type)}" } + : new object[] { $"/{ToString(s.Type)}", $"/{s.Parameter}" }) .SelectMany(s => s)); } @@ -59,7 +59,7 @@ public static Multiaddr From(params object[] segments) { if (segments[i] is not Enums.Multiaddr addr) { - throw new ArgumentException($"{segments[i]} is expceted to be a multiaddress segment id"); + throw new ArgumentException($"{segments[i]} is expected to be a multiaddress segment id"); } if (ToProto(addr).isParametrized) @@ -149,6 +149,8 @@ private static (Enums.Multiaddr type, bool isParametrized) ToProto(string val) "udp" => ToProto(Enums.Multiaddr.Udp), "p2p" => ToProto(Enums.Multiaddr.P2p), "ws" => ToProto(Enums.Multiaddr.Ws), + "quic" => ToProto(Enums.Multiaddr.Quic), + "quic-v1" => ToProto(Enums.Multiaddr.QuicV1), _ => ToProto(Enums.Multiaddr.Unknown) }; } @@ -157,12 +159,23 @@ private static (Enums.Multiaddr type, bool isParametrized) ToProto(Enums.Multiad { return val switch { + Enums.Multiaddr.Quic => (val, false), + Enums.Multiaddr.QuicV1 => (val, false), Enums.Multiaddr.Ws => (val, false), Enums.Multiaddr.Unknown => (val, false), _ => (val, true) }; } + private static string ToString(Enums.Multiaddr type) + { + return type switch + { + Enums.Multiaddr.QuicV1 => "quic-v1", + _ => type.ToString().ToLower(), + }; + } + public byte[] ToByteArray() { Span result = stackalloc byte[256]; diff --git a/src/libp2p/Libp2p.Core/PeerFactory.cs b/src/libp2p/Libp2p.Core/PeerFactory.cs index bf5ac268..4dc8adef 100644 --- a/src/libp2p/Libp2p.Core/PeerFactory.cs +++ b/src/libp2p/Libp2p.Core/PeerFactory.cs @@ -100,11 +100,9 @@ protected virtual async Task DialAsync(LocalPeer peer, Multiaddr ad Id = $"ctx-{++CtxId}", LocalPeer = peer, }; - RemotePeer result = new(this, peer, context) { Address = addr }; + RemotePeer result = new(this, peer, context) { Address = addr, Channel = chan }; context.RemotePeer = result; - _ = _protocol.DialAsync(chan, _upChannelFactory, context); - result.Channel = chan; TaskCompletionSource tcs = new(); context.OnRemotePeerConnection += remotePeer => { @@ -115,8 +113,10 @@ protected virtual async Task DialAsync(LocalPeer peer, Multiaddr ad ConnectedTo(remotePeer, true).ContinueWith((t) => { tcs.TrySetResult(true); }); }; - await tcs.Task; + _ = _protocol.DialAsync(chan, _upChannelFactory, context); + + await tcs.Task; return result; } catch diff --git a/src/libp2p/Libp2p.Core/PeerFactoryBuilderBase.cs b/src/libp2p/Libp2p.Core/PeerFactoryBuilderBase.cs index bf73982d..dd5e1786 100644 --- a/src/libp2p/Libp2p.Core/PeerFactoryBuilderBase.cs +++ b/src/libp2p/Libp2p.Core/PeerFactoryBuilderBase.cs @@ -99,7 +99,7 @@ public ProtocolStack Or(TProtocol? instance = default) where TProtoco public ProtocolStack Over(ProtocolStack stack) { - var rootProto = stack.Root ?? stack; + PeerFactoryBuilderBase.ProtocolStack rootProto = stack.Root ?? stack; TopProtocols.Add(rootProto); if (PrevSwitch != null) diff --git a/src/libp2p/Libp2p.Core/PeerId.cs b/src/libp2p/Libp2p.Core/PeerId.cs index 32a804d2..29c0a303 100644 --- a/src/libp2p/Libp2p.Core/PeerId.cs +++ b/src/libp2p/Libp2p.Core/PeerId.cs @@ -6,7 +6,6 @@ using SimpleBase; using Google.Protobuf; using Multiformats.Base; -using System.Buffers; using Nethermind.Libp2p.Core.Enums; using Multihash = Multiformats.Hash.Multihash; diff --git a/src/libp2p/Libp2p.Generators.Enums/EnumsGenerator.cs b/src/libp2p/Libp2p.Generators.Enums/EnumsGenerator.cs index 8f12f253..f6dc256e 100644 --- a/src/libp2p/Libp2p.Generators.Enums/EnumsGenerator.cs +++ b/src/libp2p/Libp2p.Generators.Enums/EnumsGenerator.cs @@ -44,7 +44,6 @@ public void Execute(GeneratorExecutionContext context) IEnumerable> grouped = vals.GroupBy(x => x.Tag); - Console.WriteLine(); foreach (IGrouping g in grouped) { diff --git a/src/libp2p/Libp2p.Generators.Protobuf/ProtobufGenerator.cs b/src/libp2p/Libp2p.Generators.Protobuf/ProtobufGenerator.cs index 5401360b..9eb42d36 100644 --- a/src/libp2p/Libp2p.Generators.Protobuf/ProtobufGenerator.cs +++ b/src/libp2p/Libp2p.Generators.Protobuf/ProtobufGenerator.cs @@ -16,7 +16,7 @@ public void Execute(GeneratorExecutionContext context) { try { - foreach (var file in context.AdditionalFiles) + foreach (AdditionalText file in context.AdditionalFiles) { Process cmd = new(); cmd.StartInfo.RedirectStandardError = true; @@ -32,7 +32,7 @@ public void Execute(GeneratorExecutionContext context) string errorLogs = cmd.StandardError.ReadToEnd(); throw new ApplicationException(errorLogs); } - var output = cmd.StandardOutput.ReadToEnd(); + string output = cmd.StandardOutput.ReadToEnd(); } } catch (Exception e) diff --git a/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs b/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs index 5e65bb11..719eea7c 100644 --- a/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs @@ -69,7 +69,7 @@ await Task.Run(async () => remoteIpEndpoint.Port); clientContext.LocalPeer.Address = context.LocalPeer.Address.Replace( context.LocalEndpoint.Has(Core.Enums.Multiaddr.Ip4) ? Core.Enums.Multiaddr.Ip4 : Core.Enums.Multiaddr.Ip6, newIpProtocol, - remoteIpEndpoint.Address.ToString()) + localIpEndpoint.Address.ToString()) .Replace( Core.Enums.Multiaddr.Tcp, remoteIpEndpoint.Port.ToString()); diff --git a/src/libp2p/Libp2p.Protocols.MDns/MDnsDiscoveryProtocol.cs b/src/libp2p/Libp2p.Protocols.MDns/MDnsDiscoveryProtocol.cs index 80d4fbfc..0b011ba9 100644 --- a/src/libp2p/Libp2p.Protocols.MDns/MDnsDiscoveryProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.MDns/MDnsDiscoveryProtocol.cs @@ -107,7 +107,7 @@ private static string RandomString(int length) return string.Create(length, Random.Shared, (chars, rand) => { - for (var i = 0; i < chars.Length; i++) + for (int i = 0; i < chars.Length; i++) chars[i] = alphabet[rand.Next(0, alphabet.Length)]; }); } diff --git a/src/libp2p/Libp2p.Protocols.Multistream.Tests/MultistreamProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Multistream.Tests/MultistreamProtocolTests.cs index 86a1b96c..9a83824d 100644 --- a/src/libp2p/Libp2p.Protocols.Multistream.Tests/MultistreamProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Multistream.Tests/MultistreamProtocolTests.cs @@ -65,6 +65,7 @@ public async Task Test_ConnectionEstablished_AfterHandshake_With_SpecificRequest Assert.That(await downChannel.ReadLineAsync(), Is.EqualTo(proto.Id)); Assert.That(await downChannel.ReadLineAsync(), Is.EqualTo("proto1")); + await Task.Delay(30); _ = channelFactory.Received().SubDialAndBind(downChannelFromProtocolPov, peerContext, proto1); await downChannel.CloseAsync(); } @@ -127,6 +128,8 @@ public async Task Test_ConnectionEstablished_ForAnyOfProtocols() Assert.That(await downChannel.ReadLineAsync(), Is.EqualTo(proto.Id)); Assert.That(await downChannel.ReadLineAsync(), Is.EqualTo(proto1.Id)); Assert.That(await downChannel.ReadLineAsync(), Is.EqualTo(proto2.Id)); + + await Task.Delay(30); _ = channelFactory.Received().SubDialAndBind(downChannelFromProtocolPov, peerContext, proto2); await upChannel.CloseAsync(); } diff --git a/src/libp2p/Libp2p.Protocols.Pubsub.Tests/FloodsubProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Pubsub.Tests/FloodsubProtocolTests.cs index 938b6572..732f1063 100644 --- a/src/libp2p/Libp2p.Protocols.Pubsub.Tests/FloodsubProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Pubsub.Tests/FloodsubProtocolTests.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited // SPDX-License-Identifier: MIT -using Nethermind.Libp2p.Core; using Nethermind.Libp2p.Protocols.Pubsub; using Nethermind.Libp2p.Protocols.Pubsub.Dto; diff --git a/src/libp2p/Libp2p.Protocols.Pubsub.Tests/GossipsubProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Pubsub.Tests/GossipsubProtocolTests.cs index b8ed140f..d137bccf 100644 --- a/src/libp2p/Libp2p.Protocols.Pubsub.Tests/GossipsubProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Pubsub.Tests/GossipsubProtocolTests.cs @@ -28,7 +28,7 @@ public async Task Test_New_messages_are_sent_to_mesh_only() Assert.That(state.FloodsubPeers.Keys, Has.Member(commonTopic)); Assert.That(state.GossipsubPeers.Keys, Has.Member(commonTopic)); - foreach (var index in Enumerable.Range(1, peerCount)) + foreach (int index in Enumerable.Range(1, peerCount)) { Multiaddr discoveredPeer = TestPeers.Multiaddr(index); PeerId peerId = TestPeers.PeerId(index); diff --git a/src/libp2p/Libp2p.Protocols.Pubsub/PubsubRouter.cs b/src/libp2p/Libp2p.Protocols.Pubsub/PubsubRouter.cs index b53efcef..530b4548 100644 --- a/src/libp2p/Libp2p.Protocols.Pubsub/PubsubRouter.cs +++ b/src/libp2p/Libp2p.Protocols.Pubsub/PubsubRouter.cs @@ -134,6 +134,7 @@ private async Task StartDiscoveryAsync(IDiscoveryProtocol discoveryProtocol, Can ObservableCollection col = new(); discoveryProtocol.OnAddPeer = (addrs) => { + //addrs = addrs.Where(x => x.ToString().Contains("127.0.0.1")).ToArray(); Dictionary cancellations = new(); foreach (Multiaddr addr in addrs) { @@ -356,7 +357,7 @@ public void Publish(string topicId, byte[] message) ulong seqNo = this.seqNo++; byte[] seqNoBytes = new byte[8]; BinaryPrimitives.WriteUInt64BigEndian(seqNoBytes, seqNo); - Rpc rpc = new Rpc().WithMessages(topicId, seqNo, LocalPeerId.Bytes, message, localPeer.Identity.PrivateKey.Data.ToArray()); + Rpc rpc = new Rpc().WithMessages(topicId, seqNo, LocalPeerId.Bytes, message, localPeer.Identity); foreach (PeerId peerId in fPeers[topicId]) { diff --git a/src/libp2p/Libp2p.Protocols.Pubsub/RpcExtensions.cs b/src/libp2p/Libp2p.Protocols.Pubsub/RpcExtensions.cs index 821d5b64..9ca82ce8 100644 --- a/src/libp2p/Libp2p.Protocols.Pubsub/RpcExtensions.cs +++ b/src/libp2p/Libp2p.Protocols.Pubsub/RpcExtensions.cs @@ -5,7 +5,6 @@ using Org.BouncyCastle.Math.EC.Rfc8032; using System.Buffers.Binary; using System.Text; -using Multiformats.Hash; using Nethermind.Libp2p.Core.Dto; using Nethermind.Libp2p.Core; using Nethermind.Libp2p.Protocols.Pubsub.Dto; @@ -17,9 +16,8 @@ internal static class RpcExtensions { private const string SignaturePayloadPrefix = "libp2p-pubsub:"; - public static Rpc WithMessages(this Rpc rpc, string topic, ulong seqNo, byte[] from, byte[] message, byte[] privateKey) + public static Rpc WithMessages(this Rpc rpc, string topic, ulong seqNo, byte[] from, byte[] message, Identity identity) { - Message msg = new(); msg.Topic = topic; Span seqNoBytes = new byte[8]; @@ -28,13 +26,11 @@ public static Rpc WithMessages(this Rpc rpc, string topic, ulong seqNo, byte[] f msg.From = ByteString.CopyFrom(from); msg.Data = ByteString.CopyFrom(message); - byte[] msgToSign = Encoding.UTF8.GetBytes(SignaturePayloadPrefix) + byte[] signingContent = Encoding.UTF8.GetBytes(SignaturePayloadPrefix) .Concat(msg.ToByteArray()) .ToArray(); - byte[] sig = new byte[64]; - Ed25519.Sign(privateKey, 0, msgToSign, 0, msgToSign.Length, sig, 0); - msg.Signature = ByteString.CopyFrom(sig); + msg.Signature = ByteString.CopyFrom(identity.Sign(signingContent)); rpc.Publish.Add(msg); return rpc; } diff --git a/src/libp2p/Libp2p.Protocols.Pubsub/TtlCache.cs b/src/libp2p/Libp2p.Protocols.Pubsub/TtlCache.cs index db9d14c4..f11c9c24 100644 --- a/src/libp2p/Libp2p.Protocols.Pubsub/TtlCache.cs +++ b/src/libp2p/Libp2p.Protocols.Pubsub/TtlCache.cs @@ -26,7 +26,7 @@ public TtlCache(int ttl) await Task.Delay(5_000); DateTimeOffset now = DateTimeOffset.UtcNow; TKey[] keys = items.TakeWhile(i => i.Value.ValidTill < now).Select(i => i.Key).ToArray(); - foreach (var keyToRemove in keys) + foreach (TKey keyToRemove in keys) { items.Remove(keyToRemove); } diff --git a/src/libp2p/Libp2p.Protocols.Quic.Tests/CertificateTests.cs b/src/libp2p/Libp2p.Protocols.Quic.Tests/CertificateTests.cs new file mode 100644 index 00000000..8f5d16c6 --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Quic.Tests/CertificateTests.cs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using Nethermind.Libp2p.Core; +using Nethermind.Libp2p.Core.Dto; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Nethermind.Libp2p.Protocols.Quic.Tests; + +public class CertificateTests +{ + [TestCaseSource(nameof(CertificatesSerialized))] + public bool Test_CertificateDeserialization(byte[] certificateBytes, string peerId) => + CertificateHelper.ValidateCertificate(new X509Certificate2(certificateBytes), peerId); + + public static IEnumerable CertificatesSerialized() + { + yield return new TestCaseData( + Convert.FromHexString("308201773082011ea003020102020900f5bd0debaa597f52300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200046bf9871220d71dcb3483ecdfcbfcc7c103f8509d0974b3c18ab1f1be1302d643103a08f7a7722c1b247ba3876fe2c59e26526f479d7718a85202ddbe47562358a37f307d307b060a2b0601040183a25a01010101ff046a30680424080112207fda21856709c5ae12fd6e8450623f15f11955d384212b89f56e7e136d2e17280440aaa6bffabe91b6f30c35e3aa4f94b1188fed96b0ffdd393f4c58c1c047854120e674ce64c788406d1c2c4b116581fd7411b309881c3c7f20b46e54c7e6fe7f0f300a06082a8648ce3d040302034700304402207d1a1dbd2bda235ff2ec87daf006f9b04ba076a5a5530180cd9c2e8f6399e09d0220458527178c7e77024601dbb1b256593e9b96d961b96349d1f560114f61a87595"), + "12D3KooWJRSrypvnpHgc6ZAgyCni4KcSmbV7uGRaMw5LgMKT18fq" + ) + { TestName = "Valid ED25519", ExpectedResult = true }; + yield return new TestCaseData( + Convert.FromHexString("308201c030820166a003020102020900eaf419a6e3edb4a6300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200048dbf1116c7c608d6d5292bd826c3feb53483a89fce434bf64538a359c8e07538ff71f6766239be6a146dcc1a5f3bb934bcd4ae2ae1d4da28ac68b4a20593f06ba381c63081c33081c0060a2b0601040183a25a01010101ff0481ae3081ab045f0803125b3059301306072a8648ce3d020106082a8648ce3d0301070342000484b93fa456a74bd0153919f036db7bc63c802f055bc7023395d0203de718ee0fc7b570b767cdd858aca6c7c4113ff002e78bd2138ac1a3b26dde3519e06979ad04483046022100bc84014cea5a41feabdf4c161096564b9ccf4b62fbef4fe1cd382c84e11101780221009204f086a84cb8ed8a9ddd7868dc90c792ee434adf62c66f99a08a5eba11615b300a06082a8648ce3d0403020348003045022054b437be9a2edf591312d68ff24bf91367ad4143f76cf80b5658f232ade820da022100e23b48de9df9c25d4c83ddddf75d2676f0b9318ee2a6c88a736d85eab94a912f"), + "QmZcrvr3r4S3QvwFdae3c2EWTfo792Y14UpzCZurhmiWeX" + ) + { TestName = "Valid ECDSA", ExpectedResult = true }; + yield return new TestCaseData( + Convert.FromHexString("3082018230820128a003020102020900f3b305f55622cfdf300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d0301070342000458f7e9581748ff9bdd933b655cc0e5552a1248f840658cc221dec2186b5a2fe4641b86ab7590a3422cdbb1000cf97662f27e5910d7569f22feed8829c8b52e0fa38188308185308182060a2b0601040183a25a01010101ff0471306f042508021221026b053094d1112bce799dc8026040ae6d4eb574157929f1598172061f753d9b1b04463044022040712707e97794c478d93989aaa28ae1f71c03af524a8a4bd2d98424948a782302207b61b7f074b696a25fb9e0059141a811cccc4cc28042d9301b9b2a4015e87470300a06082a8648ce3d04030203480030450220143ae4d86fdc8675d2480bb6912eca5e39165df7f572d836aa2f2d6acfab13f8022100831d1979a98f0c4a6fb5069ca374de92f1a1205c962a6d90ad3d7554cb7d9df4"), + "16Uiu2HAm2dSCBFxuge46aEt7U1oejtYuBUZXxASHqmcfVmk4gsbx" + ) + { TestName = "Valid secp256k1", ExpectedResult = true }; + yield return new TestCaseData( + Convert.FromHexString("308201773082011da003020102020830a73c5d896a1109300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004bbe62df9a7c1c46b7f1f21d556deec5382a36df146fb29c7f1240e60d7d5328570e3b71d99602b77a65c9b3655f62837f8d66b59f1763b8c9beba3be07778043a37f307d307b060a2b0601040183a25a01010101ff046a3068042408011220ec8094573afb9728088860864f7bcea2d4fd412fef09a8e2d24d482377c20db60440ecabae8354afa2f0af4b8d2ad871e865cb5a7c0c8d3dbdbf42de577f92461a0ebb0a28703e33581af7d2a4f2270fc37aec6261fcc95f8af08f3f4806581c730a300a06082a8648ce3d040302034800304502202dfb17a6fa0f94ee0e2e6a3b9fb6e986f311dee27392058016464bd130930a61022100ba4b937a11c8d3172b81e7cd04aedb79b978c4379c2b5b24d565dd5d67d3cb3c"), + "12D3KooWRja6riywsP8bE7V2gGg55Jsx7HrHLQcEwxvmwD8SQynV" + ) + { TestName = "Invalid", ExpectedResult = false }; + foreach (KeyType keyType in Enum.GetValues()) + { + Identity id = new(null, keyType); + yield return new TestCaseData( + CertificateHelper.CertificateFromIdentity(ECDsa.Create(), id).GetRawCertData(), + id.PeerId.ToString() + ) + { TestName = $"Roundtrip valid {keyType} key", ExpectedResult = true }; + } + } +} diff --git a/src/libp2p/Libp2p.Protocols.Quic.Tests/Libp2p.Protocols.Quic.Tests.csproj b/src/libp2p/Libp2p.Protocols.Quic.Tests/Libp2p.Protocols.Quic.Tests.csproj new file mode 100644 index 00000000..c3346f24 --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Quic.Tests/Libp2p.Protocols.Quic.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + true + Nethermind.$(MSBuildProjectName.Replace(" ", "_")) + Nethermind.$(MSBuildProjectName) + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/libp2p/Libp2p.Protocols.Quic.Tests/Usings.cs b/src/libp2p/Libp2p.Protocols.Quic.Tests/Usings.cs new file mode 100644 index 00000000..e160e2a1 --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Quic.Tests/Usings.cs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +global using NUnit.Framework; diff --git a/src/libp2p/Libp2p.Protocols.Quic/CertificateHelper.cs b/src/libp2p/Libp2p.Protocols.Quic/CertificateHelper.cs new file mode 100644 index 00000000..b26502c3 --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Quic/CertificateHelper.cs @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using Google.Protobuf; +using Nethermind.Libp2p.Core; +using System.Formats.Asn1; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Nethermind.Libp2p.Protocols.Quic; +public class CertificateHelper +{ + private const string PubkeyExtensionOidString = "1.3.6.1.4.1.53594.1.1"; + private static readonly Oid PubkeyExtensionOid = new(PubkeyExtensionOidString); + + public static X509Certificate CertificateFromIdentity(ECDsa sessionKey, Identity identity) + { + byte[] signature = identity.Sign(ContentToSignFromTlsPublicKey(sessionKey.ExportSubjectPublicKeyInfo())); + + AsnWriter asnWrtier = new(AsnEncodingRules.DER); + asnWrtier.PushSequence(); + asnWrtier.WriteOctetString(identity.PublicKey.ToByteArray()); + asnWrtier.WriteOctetString(signature); + asnWrtier.PopSequence(); + byte[] pubkeyExtension = new byte[asnWrtier.GetEncodedLength()]; + asnWrtier.Encode(pubkeyExtension); + + CertificateRequest certRequest = new("", sessionKey, HashAlgorithmName.SHA256); + certRequest.CertificateExtensions.Add(new X509Extension(PubkeyExtensionOid, pubkeyExtension, true)); + + return certRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.MaxValue); + } + + public static bool ValidateCertificate(X509Certificate2? certificate, string? peerId) + { + if (certificate is null) + { + return false; + } + + X509Extension[] exts = certificate.Extensions.Where(e => e.Oid?.Value == PubkeyExtensionOidString).ToArray(); + + if (exts.Length is 0) + { + return false; + } + + if (exts.Length is not 1) + { + return false; + } + + X509Extension ext = exts.First(); + + AsnReader a = new(ext.RawData, AsnEncodingRules.DER); + AsnReader signedKey = a.ReadSequence(); + + byte[] publicKey = signedKey.ReadOctetString(); + byte[] signature = signedKey.ReadOctetString(); + + Core.Dto.PublicKey key = Core.Dto.PublicKey.Parser.ParseFrom(publicKey); + Identity id = new(key); + if (peerId is not null && id.PeerId.ToString() != peerId) + { + return false; + } + + return id.VerifySignature(ContentToSignFromTlsPublicKey(certificate.PublicKey.ExportSubjectPublicKeyInfo()), signature); + } + + private static readonly byte[] SignaturePrefix = "libp2p-tls-handshake:"u8.ToArray(); + private static byte[] ContentToSignFromTlsPublicKey(byte[] keyInfo) => SignaturePrefix.Concat(keyInfo).ToArray(); +} diff --git a/src/libp2p/Libp2p.Protocols.Quic/Libp2p.Protocols.Quic.csproj b/src/libp2p/Libp2p.Protocols.Quic/Libp2p.Protocols.Quic.csproj index 4a949953..5c03cbaf 100644 --- a/src/libp2p/Libp2p.Protocols.Quic/Libp2p.Protocols.Quic.csproj +++ b/src/libp2p/Libp2p.Protocols.Quic/Libp2p.Protocols.Quic.csproj @@ -21,6 +21,7 @@ + all diff --git a/src/libp2p/Libp2p.Protocols.Quic/QuicProtocol.cs b/src/libp2p/Libp2p.Protocols.Quic/QuicProtocol.cs index 60555dfc..91aa2184 100644 --- a/src/libp2p/Libp2p.Protocols.Quic/QuicProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.Quic/QuicProtocol.cs @@ -3,297 +3,244 @@ using Nethermind.Libp2p.Core; using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Net.Sockets; +using MultiaddrEnum = Nethermind.Libp2p.Core.Enums.Multiaddr; +using System.Net; +using System.Net.Quic; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Nethermind.Libp2p.Protocols.Quic; +using System.Security.Cryptography; namespace Nethermind.Libp2p.Protocols; -// TODO: Rewrite with SocketAsyncEventArgs +#pragma warning disable CA1416 // Do not inform about platform compatibility +#pragma warning disable CA2252 // EnablePreviewFeatures is set in the project, but build still fails public class QuicProtocol : IProtocol { private readonly ILogger? _logger; + private readonly ECDsa _sessionKey; public QuicProtocol(ILoggerFactory? loggerFactory = null) { _logger = loggerFactory?.CreateLogger(); + _sessionKey = ECDsa.Create(); } + private static readonly List protocols = new() + { + new SslApplicationProtocol("libp2p"), + // SslApplicationProtocol.Http3, // webtransport + }; + public string Id => "quic"; - public Task ListenAsync(IChannel channel, IChannelFactory? channelFactory, IPeerContext context) + public async Task ListenAsync(IChannel channel, IChannelFactory? channelFactory, IPeerContext context) + { + if (channelFactory is null) + { + throw new ArgumentException($"The protocol requires {nameof(channelFactory)}"); + } + + if (!QuicListener.IsSupported) + { + throw new NotSupportedException("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3."); + } + + Multiaddr addr = context.LocalPeer.Address; + MultiaddrEnum ipProtocol = addr.Has(MultiaddrEnum.Ip4) ? MultiaddrEnum.Ip4 : MultiaddrEnum.Ip6; + IPAddress ipAddress = IPAddress.Parse(addr.At(ipProtocol)!); + int udpPort = int.Parse(addr.At(MultiaddrEnum.Udp)!); + + IPEndPoint localEndpoint = new(ipAddress, udpPort); + + QuicServerConnectionOptions serverConnectionOptions = new() + { + DefaultStreamErrorCode = 0, // Protocol-dependent error code. + DefaultCloseErrorCode = 1, // Protocol-dependent error code. + + ServerAuthenticationOptions = new SslServerAuthenticationOptions + { + ApplicationProtocols = protocols, + RemoteCertificateValidationCallback = (_, c, _, _) => VerifyRemoteCertificate(context.RemotePeer, c), + ServerCertificate = CertificateHelper.CertificateFromIdentity(_sessionKey, context.LocalPeer.Identity) + }, + }; + + QuicListener listener = await QuicListener.ListenAsync(new QuicListenerOptions + { + ListenEndPoint = localEndpoint, + ApplicationProtocols = protocols, + ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(serverConnectionOptions) + }); + + channel.OnClose(async () => + { + await listener.DisposeAsync(); + }); + + while (!channel.IsClosed) + { + QuicConnection connection = await listener.AcceptConnectionAsync(channel.Token); + _ = ProcessStreams(connection, context.Fork(), channelFactory, channel.Token); + } + } + + public async Task DialAsync(IChannel channel, IChannelFactory? channelFactory, IPeerContext context) + { + if (channelFactory is null) + { + throw new ArgumentException($"The protocol requires {nameof(channelFactory)}"); + } + + if (!QuicConnection.IsSupported) + { + throw new NotSupportedException("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3."); + } + + Multiaddr addr = context.LocalPeer.Address; + MultiaddrEnum ipProtocol = addr.Has(MultiaddrEnum.Ip4) ? MultiaddrEnum.Ip4 : MultiaddrEnum.Ip6; + IPAddress ipAddress = IPAddress.Parse(addr.At(ipProtocol)!); + int udpPort = int.Parse(addr.At(MultiaddrEnum.Udp)!); + + IPEndPoint localEndpoint = new(ipAddress, udpPort); + + + addr = context.RemotePeer.Address; + ipProtocol = addr.Has(MultiaddrEnum.Ip4) ? MultiaddrEnum.Ip4 : MultiaddrEnum.Ip6; + ipAddress = IPAddress.Parse(addr.At(ipProtocol)!); + udpPort = int.Parse(addr.At(MultiaddrEnum.Udp)!); + + IPEndPoint remoteEndpoint = new(ipAddress, udpPort); + + QuicClientConnectionOptions clientConnectionOptions = new() + { + LocalEndPoint = localEndpoint, + DefaultStreamErrorCode = 0, // Protocol-dependent error code. + DefaultCloseErrorCode = 1, // Protocol-dependent error code. + MaxInboundUnidirectionalStreams = 100, + MaxInboundBidirectionalStreams = 100, + ClientAuthenticationOptions = new SslClientAuthenticationOptions + { + ApplicationProtocols = protocols, + RemoteCertificateValidationCallback = (_, c, _, _) => VerifyRemoteCertificate(context.RemotePeer, c), + ClientCertificates = new X509CertificateCollection { CertificateHelper.CertificateFromIdentity(_sessionKey, context.LocalPeer.Identity) }, + }, + RemoteEndPoint = remoteEndpoint, + }; + + QuicConnection connection = await QuicConnection.ConnectAsync(clientConnectionOptions); + + channel.OnClose(async () => + { + await connection.CloseAsync(0); + await connection.DisposeAsync(); + }); + + _logger?.LogDebug($"Connected {connection.LocalEndPoint} --> {connection.RemoteEndPoint}"); + + await ProcessStreams(connection, context, channelFactory, channel.Token); + } + + private static bool VerifyRemoteCertificate(IPeer? remotePeer, X509Certificate certificate) => + CertificateHelper.ValidateCertificate(certificate as X509Certificate2, remotePeer?.Address.At(MultiaddrEnum.P2p)); + + private async Task ProcessStreams(QuicConnection connection, IPeerContext context, IChannelFactory channelFactory, CancellationToken token) { - throw new NotImplementedException(); - //MultiAddr addr = context.LocalPeer.Address; - //Multiaddr ipProtocol = addr.Has(Multiaddr.Ip4) ? Multiaddr.Ip4 : Multiaddr.Ip6; - //IPAddress ipAddress = IPAddress.Parse(addr.At(ipProtocol)!); - //int tcpPort = int.Parse(addr.At(Multiaddr.Udp)!); - - //// First, check if QUIC is supported. - //if (!QuicConnection.IsSupported) - //{ - // Console.WriteLine("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3."); - // return; - //} - - //var localEndpoint = new IPEndPoint(ipAddress, tcpPort); - //// This represents the minimal configuration necessary to open a connection. - //var clientConnectionOptions = new QuicClientConnectionOptions - //{ - // // End point of the server to connect to. - // LocalEndPoint = new IPEndPoint(ipAddress, tcpPort), - - // // Used to abort stream if it's not properly closed by the user. - // // See https://www.rfc-editor.org/rfc/rfc9000#section-20.2 - // DefaultStreamErrorCode = 0x0A, // Protocol-dependent error code. - - // // Used to close the connection if it's not done by the user. - // // See https://www.rfc-editor.org/rfc/rfc9000#section-20.2 - // DefaultCloseErrorCode = 0x0B, // Protocol-dependent error code. - - // // Optionally set limits for inbound streams. - // MaxInboundUnidirectionalStreams = 10, - // MaxInboundBidirectionalStreams = 100, - - // // Same options as for client side SslStream. - // ClientAuthenticationOptions = new SslClientAuthenticationOptions - // { - // // List of supported application protocols. - // ApplicationProtocols = channelFactory.SubProtocols.Select(proto => new SslApplicationProtocol(proto.Id)).ToList() - - // } - //}; - - - //Multiaddr newIpProtocol = localEndpoint.AddressFamily == AddressFamily.InterNetwork - // ? Multiaddr.Ip4 - // : Multiaddr.Ip6; - - //context.LocalEndpoint = MultiAddr.From(newIpProtocol, localEndpoint.Address.ToString(), - // Multiaddr.Udp, - // localEndpoint.Port); - - //context.LocalPeer.Address = context.LocalPeer.Address.Replace( - // context.LocalEndpoint.Has(Multiaddr.Ip4) ? Multiaddr.Ip4 : Multiaddr.Ip6, newIpProtocol, - // localEndpoint.Address.ToString()) - // .Replace( - // Multiaddr.Udp, - // localEndpoint.Port.ToString()); - - - //// Initialize, configure and connect to the server. - //var connection = await QuicConnection.ConnectAsync(clientConnectionOptions); - - //channel.OnClose(async () => - //{ - // await connection.CloseAsync(0); - // await connection.DisposeAsync(); - //}); - - //Console.WriteLine($"Connected {connection.LocalEndPoint} --> {connection.RemoteEndPoint}"); - - //_ = Task.Run(async () => - //{ - // while (!channel.IsClosed) - // { - // var incomingStream = await connection.AcceptInboundStreamAsync(); - // IPeerContext clientContext = context.Fork(); - // IPEndPoint remoteIpEndpoint = (IPEndPoint)client.RemoteEndPoint!; - - // clientContext.RemoteEndpoint = MultiAddr.From( - // remoteIpEndpoint.AddressFamily == AddressFamily.InterNetwork - // ? Multiaddr.Ip4 - // : Multiaddr.Ip6, remoteIpEndpoint.Address.ToString(), Multiaddr.Tcp, - // remoteIpEndpoint.Port); - // clientContext.LocalPeer.Address = context.LocalPeer.Address.Replace( - // context.LocalEndpoint.Has(Multiaddr.Ip4) ? Multiaddr.Ip4 : Multiaddr.Ip6, newIpProtocol, - // remoteIpEndpoint.Address.ToString()) - // .Replace( - // Multiaddr.Tcp, - // remoteIpEndpoint.Port.ToString()); - // clientContext.RemotePeer.Address = new MultiAddr() - // .Append(remoteIpEndpoint.AddressFamily == AddressFamily.InterNetwork - // ? Multiaddr.Ip4 - // : Multiaddr.Ip6, remoteIpEndpoint.Address.ToString()) - // .Append(Multiaddr.Tcp, remoteIpEndpoint.Port.ToString()); - - // IChannel chan = channelFactory.SubListen(clientContext); - - // _ = Task.Run(async () => - // { - // try - // { - // while (!chan.IsClosed) - // { - // if (client.Available == 0) - // { - // await Task.Yield(); - // } - - // byte[] buf = new byte[client.Available]; - // int len = await client.ReceiveAsync(buf, SocketFlags.None); - // if (len != 0) - // { - // await chan.WriteAsync(new ReadOnlySequence(buf.AsMemory()[..len])); - // } - // } - // } - // catch (SocketException) - // { - // await chan.CloseAsync(false); - // } - // }, chan.Token); - // _ = Task.Run(async () => - // { - // try - // { - // await foreach (ReadOnlySequence data in chan.ReadAllAsync()) - // { - // await client.SendAsync(data.ToArray(), SocketFlags.None); - // } - // } - // catch (SocketException) - // { - // _logger?.LogInformation("Disconnected due to a socket exception"); - // await chan.CloseAsync(false); - // } - // }, chan.Token); - // } - //}); + MultiaddrEnum newIpProtocol = connection.LocalEndPoint.AddressFamily == AddressFamily.InterNetwork + ? MultiaddrEnum.Ip4 + : MultiaddrEnum.Ip6; + + context.LocalEndpoint = Multiaddr.From( + newIpProtocol, + connection.LocalEndPoint.Address.ToString(), + MultiaddrEnum.Udp, + connection.LocalEndPoint.Port); + + context.LocalPeer.Address = context.LocalPeer.Address.Replace( + context.LocalEndpoint.Has(MultiaddrEnum.Ip4) ? MultiaddrEnum.Ip4 : MultiaddrEnum.Ip6, newIpProtocol, + connection.LocalEndPoint.Address.ToString()) + .Replace( + MultiaddrEnum.Udp, + connection.LocalEndPoint.Port.ToString()); + + IPEndPoint remoteIpEndpoint = connection.RemoteEndPoint!; + newIpProtocol = remoteIpEndpoint.AddressFamily == AddressFamily.InterNetwork + ? MultiaddrEnum.Ip4 + : MultiaddrEnum.Ip6; + + context.RemoteEndpoint = Multiaddr.From( + newIpProtocol, + remoteIpEndpoint.Address.ToString(), + MultiaddrEnum.Udp, + remoteIpEndpoint.Port); + + context.Connected(context.RemotePeer); + + _ = Task.Run(async () => + { + foreach (IChannelRequest request in context.SubDialRequests.GetConsumingEnumerable()) + { + QuicStream stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional); + IPeerContext dialContext = context.Fork(); + dialContext.SpecificProtocolRequest = request; + IChannel upChannel = channelFactory.SubDial(dialContext); + ExchangeData(stream, upChannel, request.CompletionSource); + } + }, token); + + while (!token.IsCancellationRequested) + { + QuicStream inboundStream = await connection.AcceptInboundStreamAsync(token); + IChannel upChannel = channelFactory.SubListen(context); + ExchangeData(inboundStream, upChannel, null); + } } - public Task DialAsync(IChannel channel, IChannelFactory? channelFactory, IPeerContext context) + private void ExchangeData(QuicStream stream, IChannel upChannel, TaskCompletionSource? tcs) { - throw new NotImplementedException(); - //// First, check if QUIC is supported. - //if (!QuicConnection.IsSupported) - //{ - // Console.WriteLine("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3."); - // return; - //} - - //// This represents the minimal configuration necessary to open a connection. - //var clientConnectionOptions = new QuicClientConnectionOptions - //{ - // // End point of the server to connect to. - // RemoteEndPoint = listener.LocalEndPoint, - - // // Used to abort stream if it's not properly closed by the user. - // // See https://www.rfc-editor.org/rfc/rfc9000#section-20.2 - // DefaultStreamErrorCode = 0x0A, // Protocol-dependent error code. - - // // Used to close the connection if it's not done by the user. - // // See https://www.rfc-editor.org/rfc/rfc9000#section-20.2 - // DefaultCloseErrorCode = 0x0B, // Protocol-dependent error code. - - // // Optionally set limits for inbound streams. - // MaxInboundUnidirectionalStreams = 10, - // MaxInboundBidirectionalStreams = 100, - - // // Same options as for client side SslStream. - // ClientAuthenticationOptions = new SslClientAuthenticationOptions - // { - // // List of supported application protocols. - // ApplicationProtocols = channelFactory.SubProtocols.Select(proto => new SslApplicationProtocol(proto.Id)).ToList() - // } - //}; - - //// Initialize, configure and connect to the server. - //var connection = await QuicConnection.ConnectAsync(clientConnectionOptions); - - //Console.WriteLine($"Connected {connection.LocalEndPoint} --> {connection.RemoteEndPoint}"); - - //// Open a bidirectional (can both read and write) outbound stream. - //var outgoingStream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional); - - //// Work with the outgoing stream ... - - //// To accept any stream on a client connection, at least one of MaxInboundBidirectionalStreams or MaxInboundUnidirectionalStreams of QuicConnectionOptions must be set. - //while (isRunning) - //{ - // // Accept an inbound stream. - // var incomingStream = await connection.AcceptInboundStreamAsync(); - - // // Work with the incoming stream ... - //} - - //// Close the connection with the custom code. - //await connection.CloseAsync(0x0C); - - //// Dispose the connection. - //await connection.DisposeAsync(); - - - //TaskCompletionSource waitForStop = new(TaskCreationOptions.RunContinuationsAsynchronously); - //Socket client = new(SocketType.Stream, ProtocolType.Tcp); - //MultiAddr addr = context.RemotePeer.Address; - //Multiaddr ipProtocol = addr.Has(Multiaddr.Ip4) ? Multiaddr.Ip4 : Multiaddr.Ip6; - //IPAddress ipAddress = IPAddress.Parse(addr.At(ipProtocol)!); - //int tcpPort = int.Parse(addr.At(Multiaddr.Tcp)!); - //try - //{ - // await client.ConnectAsync(new IPEndPoint(ipAddress, tcpPort)); - //} - //catch (SocketException) - //{ - // _logger?.LogInformation("Failed to connect"); - // // TODO: Add proper exception and reconnection handling - // return; - //} - - //IPEndPoint localEndpoint = (IPEndPoint)client.LocalEndPoint!; - //IPEndPoint remoteEndpoint = (IPEndPoint)client.RemoteEndPoint!; - - //context.RemoteEndpoint = MultiAddr.From( - // ipProtocol, - // ipProtocol == Multiaddr.Ip4 ? remoteEndpoint.Address.MapToIPv4() : remoteEndpoint.Address.MapToIPv6(), - // Multiaddr.Tcp, remoteEndpoint.Port); - //context.LocalEndpoint = MultiAddr.From( - // ipProtocol, - // ipProtocol == Multiaddr.Ip4 ? localEndpoint.Address.MapToIPv4() : localEndpoint.Address.MapToIPv6(), - // Multiaddr.Tcp, localEndpoint.Port); - //context.LocalPeer.Address = context.LocalEndpoint.Append(Multiaddr.P2p, context.LocalPeer.Identity.PeerId); - - //IChannel upChannel = channelFactory.SubDial(context); - ////upChannel.OnClosing += (graceful) => upChannel.CloseAsync(graceful); - - //_ = Task.Run(async () => - //{ - // byte[] buf = new byte[client.ReceiveBufferSize]; - // try - // { - // while (!upChannel.IsClosed) - // { - // int len = await client.ReceiveAsync(buf, SocketFlags.None); - // if (len != 0) - // { - // _logger?.LogDebug("Receive data, len={0}", len); - // await upChannel.WriteAsync(new ReadOnlySequence(buf[..len])); - // } - // } - - // waitForStop.SetCanceled(); - // } - // catch (SocketException) - // { - // await upChannel.CloseAsync(); - // waitForStop.SetCanceled(); - // } - //}); - - //_ = Task.Run(async () => - //{ - // try - // { - // await foreach (ReadOnlySequence data in upChannel.ReadAllAsync()) - // { - // await client.SendAsync(data.ToArray(), SocketFlags.None); - // } - - // waitForStop.SetCanceled(); - // } - // catch (SocketException) - // { - // await upChannel.CloseAsync(false); - // waitForStop.SetCanceled(); - // } - //}); + upChannel.OnClose(async () => + { + tcs?.SetResult(); + stream.Close(); + }); + + _ = Task.Run(async () => + { + try + { + await foreach (ReadOnlySequence data in upChannel.ReadAllAsync()) + { + await stream.WriteAsync(data.ToArray(), upChannel.Token); + } + } + catch (SocketException) + { + _logger?.LogInformation("Disconnected due to a socket exception"); + await upChannel.CloseAsync(false); + } + }, upChannel.Token); + + _ = Task.Run(async () => + { + try + { + while (!upChannel.IsClosed) + { + byte[] buf = new byte[1024]; + int len = await stream.ReadAtLeastAsync(buf, 1, false, upChannel.Token); + if (len != 0) + { + await upChannel.WriteAsync(new ReadOnlySequence(buf.AsMemory()[..len])); + } + } + } + catch (SocketException) + { + _logger?.LogInformation("Disconnected due to a socket exception"); + await upChannel.CloseAsync(false); + } + }); } } diff --git a/src/libp2p/Libp2p.Protocols.Quic/README.md b/src/libp2p/Libp2p.Protocols.Quic/README.md index 8d3541e5..8a1c855c 100644 --- a/src/libp2p/Libp2p.Protocols.Quic/README.md +++ b/src/libp2p/Libp2p.Protocols.Quic/README.md @@ -1,3 +1,18 @@ # QUIC transport See the [libp2p spec](https://github.com/libp2p/specs/tree/master/quic) + +## Native dependencies + +### Windows + +Quic support is fully inbuilt in the runtime. By default it utilizes `Schannel`-based implementation which is not compatible with libp2p, `OpenSSL`-based version is required. To use it instead of standard one, a runtime library needs to be replaced: + +1. Locate the current NETCore runtime: `dotnet --info` +2. Find `msquic.dll` for the current version and replace it with one from the project. + +Typical location of `msquic.dll` is "C:\Program Files\dotnet\shared\Microsoft.NETCore.App\x.y.zzz\msquic.dll" + +### Linux + +`libmsquic.so` is required , as described [here](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Quic/readme.md#linux). diff --git a/src/libp2p/Libp2p.Protocols.Quic/msquic.dll b/src/libp2p/Libp2p.Protocols.Quic/msquic.dll new file mode 100644 index 00000000..6e1adf37 Binary files /dev/null and b/src/libp2p/Libp2p.Protocols.Quic/msquic.dll differ diff --git a/src/libp2p/Libp2p.sln b/src/libp2p/Libp2p.sln index 13453f2b..7ca6e42f 100644 --- a/src/libp2p/Libp2p.sln +++ b/src/libp2p/Libp2p.sln @@ -55,6 +55,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libp2p.Protocols.Ping", "Li EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Multiformats.Hash", "..\cs-multihash\src\Multiformats.Hash\Multiformats.Hash.csproj", "{064158B8-C0A0-4CE5-8D6A-77FE657788FE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libp2p.Protocols.Quic.Tests", "Libp2p.Protocols.Quic.Tests\Libp2p.Protocols.Quic.Tests.csproj", "{EEECB761-A3C3-4598-AD03-EFABBF6CAA77}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -157,6 +159,10 @@ Global {064158B8-C0A0-4CE5-8D6A-77FE657788FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {064158B8-C0A0-4CE5-8D6A-77FE657788FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {064158B8-C0A0-4CE5-8D6A-77FE657788FE}.Release|Any CPU.Build.0 = Release|Any CPU + {EEECB761-A3C3-4598-AD03-EFABBF6CAA77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEECB761-A3C3-4598-AD03-EFABBF6CAA77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEECB761-A3C3-4598-AD03-EFABBF6CAA77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEECB761-A3C3-4598-AD03-EFABBF6CAA77}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -178,6 +184,7 @@ Global {F4546A3C-A37C-4E4E-BD8C-BE8A70C2A4A8} = {6F3D9AA9-C92D-4998-BC4E-D5EA068E8D0D} {E4103D59-03EB-488A-8392-0D2FBE3FBCC3} = {6F3D9AA9-C92D-4998-BC4E-D5EA068E8D0D} {FC0E9BCE-2848-45DC-AE20-FB7E862A199E} = {6F3D9AA9-C92D-4998-BC4E-D5EA068E8D0D} + {EEECB761-A3C3-4598-AD03-EFABBF6CAA77} = {6F3D9AA9-C92D-4998-BC4E-D5EA068E8D0D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E337E37C-3DB8-42FA-9A83-AC4E3B2557B4} diff --git a/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs b/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs index 157316fd..76048572 100644 --- a/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs +++ b/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs @@ -33,15 +33,12 @@ protected override ProtocolStack BuildStack() .Over() .Over(tcpEncryptionStack) .Over() - .Over() - .Over(); - - ProtocolStack quicStack = - Over(); + .Over(); return Over() - .Over(quicStack).Or(tcpStack) + .Over().Or(tcpStack) + .Over() .AddAppLayerProtocol() //.AddAppLayerProtocol() //.AddAppLayerProtocol() diff --git a/src/libp2p/Libp2p/MultiaddrBasedSelectorProtocol.cs b/src/libp2p/Libp2p/MultiaddrBasedSelectorProtocol.cs index 7c999703..6e993342 100644 --- a/src/libp2p/Libp2p/MultiaddrBasedSelectorProtocol.cs +++ b/src/libp2p/Libp2p/MultiaddrBasedSelectorProtocol.cs @@ -18,9 +18,23 @@ public class MultiaddrBasedSelectorProtocol(ILoggerFactory? loggerFactory = null protected override async Task ConnectAsync(IChannel _, IChannelFactory? channelFactory, IPeerContext context, bool isListener) { - IProtocol protocol = context.LocalPeer.Address.Has(Core.Enums.Multiaddr.Quic) ? - channelFactory!.SubProtocols.FirstOrDefault(proto => proto.Id.Contains("quic")) ?? throw new ApplicationException("QUIC is not supported") : - channelFactory!.SubProtocols.FirstOrDefault(proto => proto.Id.Contains("tcp")) ?? throw new ApplicationException("TCP is not supported"); + IProtocol protocol = null!; + if (context.LocalPeer.Address.Has(Core.Enums.Multiaddr.QuicV1)) + { + protocol = channelFactory!.SubProtocols.FirstOrDefault(proto => proto.Id.Contains("quic")) ?? throw new ApplicationException("QUIC is not supported"); + } + else if (context.LocalPeer.Address.Has(Core.Enums.Multiaddr.Quic)) + { + throw new ApplicationException("QUIC version draft-29 is not supported."); + } + else if (context.LocalPeer.Address.Has(Core.Enums.Multiaddr.Tcp)) + { + protocol = channelFactory!.SubProtocols.FirstOrDefault(proto => proto.Id.Contains("tcp")) ?? throw new ApplicationException("TCP is not supported"); + } + else + { + throw new NotImplementedException($"No transport protocol found for the given address: {context.LocalPeer.Address}"); + } _logger?.LogPickedProtocol(protocol.Id, isListener ? "listen" : "dial"); diff --git a/src/samples/chat/Program.cs b/src/samples/chat/Program.cs index 9520d8ad..83614fd8 100644 --- a/src/samples/chat/Program.cs +++ b/src/samples/chat/Program.cs @@ -39,7 +39,7 @@ ILocalPeer peer = peerFactory.Create(optionalFixedIdentity); IListener listener = await peer.ListenAsync( - $"/ip4/0.0.0.0/tcp/{(args.Length > 0 && args[0] == "-sp" ? args[1] : "0")}/p2p/{peer.Identity.PeerId}", + $"/ip4/0.0.0.0/udp/{(args.Length > 0 && args[0] == "-sp" ? args[1] : "0")}/quic-v1/p2p/{peer.Identity.PeerId}", ts.Token); logger.LogInformation($"Listener started at {listener.Address}"); listener.OnConnection += async remotePeer => logger.LogInformation($"A peer connected {remotePeer.Address}"); diff --git a/src/samples/pubsub-chat/Program.cs b/src/samples/pubsub-chat/Program.cs index 51987414..c68b5501 100644 --- a/src/samples/pubsub-chat/Program.cs +++ b/src/samples/pubsub-chat/Program.cs @@ -27,13 +27,10 @@ ILogger logger = serviceProvider.GetService()!.CreateLogger("Pubsub Chat"); CancellationTokenSource ts = new(); -Random r = new(); -byte[] buf = new byte[32]; -r.NextBytes(buf); -Identity optionalFixedIdentity = new(buf); -string addr = $"/ip4/0.0.0.0/tcp/0/p2p/{optionalFixedIdentity.PeerId}"; +Identity localPeerIdentity = new(); +string addr = $"/ip4/0.0.0.0/udp/0/quic-v1/p2p/{localPeerIdentity.PeerId}"; -ILocalPeer peer = peerFactory.Create(optionalFixedIdentity, addr); +ILocalPeer peer = peerFactory.Create(localPeerIdentity, addr); PubsubRouter router = serviceProvider.GetService()!; ITopic topic = router.Subscribe("chat-room:awesome-chat-room");