Expose local endpoint in channel adapter (#2213)

* Expose local endpoint

* Update release notes
This commit is contained in:
Christian
2025-10-19 23:06:36 +02:00
committed by GitHub
parent 5ebc5fcb7e
commit e791d9aba4
14 changed files with 208 additions and 150 deletions
@@ -61,27 +61,6 @@ public sealed class MqttConnectionContext : IMqttChannelAdapter
} }
} }
public EndPoint RemoteEndPoint
{
get
{
// mqtt over tcp
if (_connection.RemoteEndPoint != null)
{
return _connection.RemoteEndPoint;
}
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpConnectionFeature>();
if (httpFeature?.RemoteIpAddress != null)
{
return new IPEndPoint(httpFeature.RemoteIpAddress, httpFeature.RemotePort);
}
return null;
}
}
public bool IsSecureConnection public bool IsSecureConnection
{ {
get get
@@ -104,8 +83,50 @@ public sealed class MqttConnectionContext : IMqttChannelAdapter
} }
} }
public EndPoint LocalEndPoint
{
get
{
// mqtt over tcp
if (_connection.LocalEndPoint != null)
{
return _connection.LocalEndPoint;
}
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpConnectionFeature>();
if (httpFeature?.LocalIpAddress != null)
{
return new IPEndPoint(httpFeature.LocalIpAddress, httpFeature.LocalPort);
}
return null;
}
}
public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } public MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
public EndPoint RemoteEndPoint
{
get
{
// mqtt over tcp
if (_connection.RemoteEndPoint != null)
{
return _connection.RemoteEndPoint;
}
// mqtt over websocket
var httpFeature = _connection.Features.Get<IHttpConnectionFeature>();
if (httpFeature?.RemoteIpAddress != null)
{
return new IPEndPoint(httpFeature.RemoteIpAddress, httpFeature.RemotePort);
}
return null;
}
}
public async Task ConnectAsync(CancellationToken cancellationToken) public async Task ConnectAsync(CancellationToken cancellationToken)
{ {
if (_connection is SocketConnection tcp && !tcp.IsConnected) if (_connection is SocketConnection tcp && !tcp.IsConnected)
@@ -232,8 +253,8 @@ public sealed class MqttConnectionContext : IMqttChannelAdapter
var span = output.GetSpan(buffer.Length); var span = output.GetSpan(buffer.Length);
buffer.Packet.AsSpan().CopyTo(span); buffer.Packet.AsSpan().CopyTo(span);
int offset = buffer.Packet.Count; var offset = buffer.Packet.Count;
buffer.Payload.CopyTo(destination: span.Slice(offset)); buffer.Payload.CopyTo(span.Slice(offset));
output.Advance(buffer.Length); output.Advance(buffer.Length);
} }
} }
@@ -79,6 +79,8 @@ public class SerializerBenchmark : BaseBenchmark
public EndPoint RemoteEndPoint { get; set; } public EndPoint RemoteEndPoint { get; set; }
public EndPoint LocalEndPoint { get; set; }
public bool IsSecureConnection { get; } = false; public bool IsSecureConnection { get; } = false;
public X509Certificate2 ClientCertificate { get; set; } public X509Certificate2 ClientCertificate { get; set; }
@@ -211,7 +211,7 @@ public sealed class MqttTcpServerListener : IDisposable
var clientHandler = ClientHandler; var clientHandler = ClientHandler;
if (clientHandler != null) if (clientHandler != null)
{ {
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate); var tcpChannel = new MqttTcpChannel(stream, _localEndPoint, remoteEndPoint, clientCertificate);
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax); var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
@@ -14,6 +14,8 @@
<NuGetAudit>true</NuGetAudit> <NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel> <NuGetAuditLevel>low</NuGetAuditLevel>
<AnalysisLevel>latest-Recommended</AnalysisLevel> <AnalysisLevel>latest-Recommended</AnalysisLevel>
<LangVersion>default</LangVersion>
<Nullable>disable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -29,6 +29,8 @@ public sealed class MemoryMqttChannel : IMqttChannel
public EndPoint RemoteEndPoint { get; set; } public EndPoint RemoteEndPoint { get; set; }
public EndPoint LocalEndPoint { get; set; }
public bool IsSecureConnection { get; } = false; public bool IsSecureConnection { get; } = false;
public X509Certificate2 ClientCertificate { get; set; } public X509Certificate2 ClientCertificate { get; set; }
+1 -1
View File
@@ -45,7 +45,7 @@ public class MqttTcpChannel_Tests
using var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); using var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp);
await clientSocket.ConnectAsync(remoteEndPoint, CancellationToken.None); await clientSocket.ConnectAsync(remoteEndPoint, CancellationToken.None);
var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), remoteEndPoint, null); var tcpChannel = new MqttTcpChannel(clientSocket.GetStream(), new DnsEndPoint("localhost", 50000), remoteEndPoint, null);
await Task.Delay(100, ct.Token); await Task.Delay(100, ct.Token);
+96 -98
View File
@@ -2,11 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Server;
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
@@ -14,12 +9,18 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Server;
namespace MQTTnet.Tests.Server; namespace MQTTnet.Tests.Server;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
[TestClass] [TestClass]
public sealed class General_Tests : BaseTestClass public class General_Tests : BaseTestClass
{ {
Dictionary<string, bool> _connected; Dictionary<string, bool> _connected;
@@ -83,23 +84,53 @@ public sealed class General_Tests : BaseTestClass
} }
[TestMethod] [TestMethod]
public async Task Deny_Connection() public async Task Disconnect_Client_with_Reason()
{ {
using var testEnvironment = CreateTestEnvironment(); using var testEnvironment = CreateTestEnvironment();
testEnvironment.IgnoreClientLogErrors = true; var disconnectPacketReceived = false;
var server = await testEnvironment.StartServer(); string testClientId = null;
server.ValidatingConnectionAsync += e => await testEnvironment.StartServer();
testEnvironment.Server.ClientConnectedAsync += e =>
{ {
e.ReasonCode = MqttConnectReasonCode.NotAuthorized; testClientId = e.ClientId;
return CompletedTask.Instance; return CompletedTask.Instance;
}; };
var client = testEnvironment.CreateClient(); var client = testEnvironment.CreateClient();
var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptions());
Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, response.ResultCode); client.InspectPacketAsync += e =>
{
if (e.Buffer.Length > 0)
{
if (e.Buffer[0] == (byte)MqttControlPacketType.Disconnect << 4)
{
disconnectPacketReceived = true;
}
}
return CompletedTask.Instance;
};
await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithProtocolVersion(MqttProtocolVersion.V311).Build());
await LongTestDelay();
// Test client should be connected now
Assert.IsNotNull(testClientId);
// Have the server disconnect the client with SessionTakenOver reason; no DISCONNECT should be sent to MQTT 3.1.1 clients
await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.SessionTakenOver);
await LongTestDelay();
// MQTT 3.1.1 client should not receive a disconnect packet
Assert.IsFalse(disconnectPacketReceived, "Disconnect packet received when none is expected");
} }
[TestMethod] [TestMethod]
@@ -152,6 +183,27 @@ public sealed class General_Tests : BaseTestClass
Assert.AreEqual("y", buffer.ToString()); Assert.AreEqual("y", buffer.ToString());
} }
[TestMethod]
public async Task Expose_Local_And_Remote_Endpoint()
{
using var testEnvironment = CreateTestEnvironment();
var server = await testEnvironment.StartServer();
ValidatingConnectionEventArgs eventArgs = null;
server.ValidatingConnectionAsync += e =>
{
eventArgs = e;
return Task.CompletedTask;
};
await testEnvironment.ConnectClient();
await Task.Delay(500);
Assert.AreEqual("0.0.0.0:" + testEnvironment.ServerPort, eventArgs.ChannelAdapter.LocalEndPoint.ToString());
Assert.StartsWith("127.0.0.1:", eventArgs.ChannelAdapter.RemoteEndPoint.ToString());
}
[TestMethod] [TestMethod]
public async Task Handle_Clean_Disconnect() public async Task Handle_Clean_Disconnect()
{ {
@@ -205,35 +257,34 @@ public sealed class General_Tests : BaseTestClass
var testEnvironment2 = testEnvironment; var testEnvironment2 = testEnvironment;
tasks.Add( tasks.Add(
Task.Run( Task.Run(async () =>
async () => {
try
{ {
try using var client = await testEnvironment2.ConnectClient();
{ // Clear retained message.
using var client = await testEnvironment2.ConnectClient(); await client.PublishAsync(
// Clear retained message. new MqttApplicationMessageBuilder().WithTopic("r" + i2)
await client.PublishAsync( .WithPayload(EmptyBuffer.Array)
new MqttApplicationMessageBuilder().WithTopic("r" + i2) .WithRetainFlag()
.WithPayload(EmptyBuffer.Array) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.WithRetainFlag() .Build());
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build());
// Set retained message. // Set retained message.
await client.PublishAsync( await client.PublishAsync(
new MqttApplicationMessageBuilder().WithTopic("r" + i2) new MqttApplicationMessageBuilder().WithTopic("r" + i2)
.WithPayload("value") .WithPayload("value")
.WithRetainFlag() .WithRetainFlag()
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build()); .Build());
await client.DisconnectAsync(); await client.DisconnectAsync();
} }
catch (Exception exception) catch (Exception exception)
{ {
testEnvironment2.TrackException(exception); testEnvironment2.TrackException(exception);
} }
})); }));
await Task.Delay(10); await Task.Delay(10);
} }
@@ -432,21 +483,21 @@ public sealed class General_Tests : BaseTestClass
} }
[TestMethod] [TestMethod]
public async Task Publish_At_Least_Once_0x01() public Task Publish_At_Least_Once_0x01()
{ {
await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, "A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, 1); return TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, "A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, 1);
} }
[TestMethod] [TestMethod]
public async Task Publish_At_Most_Once_0x00() public Task Publish_At_Most_Once_0x00()
{ {
await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "A/B/C", MqttQualityOfServiceLevel.AtMostOnce, 1); return TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "A/B/C", MqttQualityOfServiceLevel.AtMostOnce, 1);
} }
[TestMethod] [TestMethod]
public async Task Publish_Exactly_Once_0x02() public Task Publish_Exactly_Once_0x02()
{ {
await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, "A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, 1); return TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, "A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, 1);
} }
[TestMethod] [TestMethod]
@@ -886,59 +937,6 @@ public sealed class General_Tests : BaseTestClass
Assert.IsTrue(client.IsConnected); Assert.IsTrue(client.IsConnected);
} }
[TestMethod]
public async Task Disconnect_Client_with_Reason()
{
using var testEnvironment = CreateTestEnvironment();
var disconnectPacketReceived = false;
string testClientId = null;
await testEnvironment.StartServer();
testEnvironment.Server.ClientConnectedAsync += e =>
{
testClientId = e.ClientId;
return CompletedTask.Instance;
};
var client = testEnvironment.CreateClient();
client.InspectPacketAsync += e =>
{
if (e.Buffer.Length > 0)
{
if (e.Buffer[0] == (byte)MqttControlPacketType.Disconnect << 4)
{
disconnectPacketReceived = true;
}
}
return CompletedTask.Instance;
};
await client.ConnectAsync(
new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort)
.WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V311)
.Build()
);
await LongTestDelay();
// Test client should be connected now
Assert.IsNotNull(testClientId);
// Have the server disconnect the client with SessionTakenOver reason; no DISCONNECT should be sent to MQTT 3.1.1 clients
await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.SessionTakenOver);
await LongTestDelay();
// MQTT 3.1.1 client should not receive a disconnect packet
Assert.IsFalse(disconnectPacketReceived, "Disconnect packet received when none is expected");
}
void ConnectionValidationHandler(ValidatingConnectionEventArgs eventArgs) void ConnectionValidationHandler(ValidatingConnectionEventArgs eventArgs)
{ {
if (!_connected.TryAdd(eventArgs.ClientId, true)) if (!_connected.TryAdd(eventArgs.ClientId, true))
+21 -1
View File
@@ -14,8 +14,28 @@ namespace MQTTnet.Tests.Server;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
[TestClass] [TestClass]
public sealed class Security_Tests : BaseTestClass public class Security_Tests : BaseTestClass
{ {
[TestMethod]
public async Task Deny_Connection()
{
using var testEnvironment = CreateTestEnvironment();
testEnvironment.IgnoreClientLogErrors = true;
var server = await testEnvironment.StartServer();
server.ValidatingConnectionAsync += e =>
{
e.ReasonCode = MqttConnectReasonCode.NotAuthorized;
return CompletedTask.Instance;
};
var client = testEnvironment.CreateClient();
var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptions());
Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, response.ResultCode);
}
[TestMethod] [TestMethod]
public async Task Do_Not_Affect_Authorized_Clients() public async Task Do_Not_Affect_Authorized_Clients()
{ {
@@ -22,6 +22,8 @@ public interface IMqttChannelAdapter : IDisposable
EndPoint RemoteEndPoint { get; } EndPoint RemoteEndPoint { get; }
EndPoint LocalEndPoint { get; }
bool IsSecureConnection { get; } bool IsSecureConnection { get; }
MqttPacketFormatterAdapter PacketFormatterAdapter { get; } MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
@@ -54,6 +54,8 @@ public sealed class MqttChannelAdapter : Disposable, IMqttChannelAdapter
public EndPoint RemoteEndPoint => _channel.RemoteEndPoint; public EndPoint RemoteEndPoint => _channel.RemoteEndPoint;
public EndPoint LocalEndPoint => _channel.LocalEndPoint;
public bool IsSecureConnection => _channel.IsSecureConnection; public bool IsSecureConnection => _channel.IsSecureConnection;
public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } public MqttPacketFormatterAdapter PacketFormatterAdapter { get; }
+3
View File
@@ -14,8 +14,11 @@ namespace MQTTnet.Channel;
public interface IMqttChannel : IDisposable public interface IMqttChannel : IDisposable
{ {
X509Certificate2 ClientCertificate { get; } X509Certificate2 ClientCertificate { get; }
EndPoint RemoteEndPoint { get; } EndPoint RemoteEndPoint { get; }
EndPoint LocalEndPoint { get; }
bool IsSecureConnection { get; } bool IsSecureConnection { get; }
Task ConnectAsync(CancellationToken cancellationToken); Task ConnectAsync(CancellationToken cancellationToken);
@@ -37,10 +37,11 @@ public sealed class MqttTcpChannel : IMqttChannel
IsSecureConnection = clientOptions.ChannelOptions?.TlsOptions?.UseTls == true; IsSecureConnection = clientOptions.ChannelOptions?.TlsOptions?.UseTls == true;
} }
public MqttTcpChannel(Stream stream, EndPoint remoteEndPoint, X509Certificate2 clientCertificate) : this() public MqttTcpChannel(Stream stream, EndPoint localEndPoint, EndPoint remoteEndPoint, X509Certificate2 clientCertificate) : this()
{ {
_stream = stream ?? throw new ArgumentNullException(nameof(stream)); _stream = stream ?? throw new ArgumentNullException(nameof(stream));
LocalEndPoint = localEndPoint;
RemoteEndPoint = remoteEndPoint; RemoteEndPoint = remoteEndPoint;
IsSecureConnection = stream is SslStream; IsSecureConnection = stream is SslStream;
@@ -49,10 +50,13 @@ public sealed class MqttTcpChannel : IMqttChannel
public X509Certificate2 ClientCertificate { get; } public X509Certificate2 ClientCertificate { get; }
public EndPoint RemoteEndPoint { get; private set; }
public bool IsSecureConnection { get; } public bool IsSecureConnection { get; }
public EndPoint LocalEndPoint { get; private set; }
public EndPoint RemoteEndPoint { get; private set; }
public async Task ConnectAsync(CancellationToken cancellationToken) public async Task ConnectAsync(CancellationToken cancellationToken)
{ {
CrossPlatformSocket socket = null; CrossPlatformSocket socket = null;
@@ -87,7 +91,7 @@ public sealed class MqttTcpChannel : IMqttChannel
socket.LingerState = _tcpOptions.LingerState; socket.LingerState = _tcpOptions.LingerState;
} }
if (_tcpOptions.DualMode.HasValue) if (_tcpOptions.DualMode != null)
{ {
// It is important to avoid setting the flag if no specific value is set by the user // It is important to avoid setting the flag if no specific value is set by the user
// because on IPv4 only networks the setter will always throw an exception. Regardless // because on IPv4 only networks the setter will always throw an exception. Regardless
@@ -115,37 +119,20 @@ public sealed class MqttTcpChannel : IMqttChannel
SslStream sslStream; SslStream sslStream;
if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null) if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null)
{ {
sslStream = new SslStream( sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback, InternalUserCertificateSelectionCallback);
networkStream,
false,
InternalUserCertificateValidationCallback,
InternalUserCertificateSelectionCallback);
} }
else else
{ {
// Use a different constructor depending on the options for MQTTnet so that we do not have // Use a different constructor depending on the options for MQTTnet so that we do not have
// to copy the exact same behavior of the selection handler. // to copy the exact same behavior of the selection handler.
sslStream = new SslStream( sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback);
networkStream,
false,
InternalUserCertificateValidationCallback);
} }
try try
{ {
var sslOptions = new SslClientAuthenticationOptions var sslOptions = CreateSslAuthenticationOptions();
{
ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, sslOptions.TargetHost = targetHost;
ClientCertificates = LoadCertificates(),
EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol,
CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors
? X509RevocationMode.NoCheck
: _tcpOptions.TlsOptions.RevocationMode,
TargetHost = targetHost,
CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy,
EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy,
AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation
};
if (_tcpOptions.TlsOptions.TrustChain?.Count > 0) if (_tcpOptions.TlsOptions.TrustChain?.Count > 0)
{ {
@@ -176,6 +163,7 @@ public sealed class MqttTcpChannel : IMqttChannel
} }
RemoteEndPoint = socket.RemoteEndPoint; RemoteEndPoint = socket.RemoteEndPoint;
LocalEndPoint = socket.LocalEndPoint;
} }
catch catch
{ {
@@ -281,6 +269,21 @@ public sealed class MqttTcpChannel : IMqttChannel
} }
} }
SslClientAuthenticationOptions CreateSslAuthenticationOptions()
{
var sslOptions = new SslClientAuthenticationOptions
{
ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols,
ClientCertificates = LoadCertificates(),
EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol,
CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode,
CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy,
EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy,
AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation
};
return sslOptions;
}
X509Certificate InternalUserCertificateSelectionCallback( X509Certificate InternalUserCertificateSelectionCallback(
object sender, object sender,
string targetHost, string targetHost,
@@ -39,6 +39,8 @@ public sealed class MqttWebSocketChannel : IMqttChannel
public EndPoint RemoteEndPoint { get; } public EndPoint RemoteEndPoint { get; }
public EndPoint LocalEndPoint { get; }
public bool IsSecureConnection { get; private set; } public bool IsSecureConnection { get; private set; }
public async Task ConnectAsync(CancellationToken cancellationToken) public async Task ConnectAsync(CancellationToken cancellationToken)
+1
View File
@@ -2,6 +2,7 @@
* Core: Performance improvements * Core: Performance improvements
* Server: Improved performance of retained messages when no event handler is attached (#2093, thanks to @zhaowgit) * Server: Improved performance of retained messages when no event handler is attached (#2093, thanks to @zhaowgit)
* Server: The event `InterceptingClientEnqueue` is now also called for retained messages (BREAKING CHANGE!) * Server: The event `InterceptingClientEnqueue` is now also called for retained messages (BREAKING CHANGE!)
* Server: The local end point is now also exposed in the channel adapter (#2179)
* Marked all projects as AOT compatible * Marked all projects as AOT compatible
* Restored the strong name of the nugets * Restored the strong name of the nugets
* Embedded license file in all nugets (#2197, thanks to @JensNordenbro) * Embedded license file in all nugets (#2197, thanks to @JensNordenbro)