diff --git a/src/Thinktecture.Relay.Connector/DependencyInjection/RelayConnectorBuilderExtensions.cs b/src/Thinktecture.Relay.Connector/DependencyInjection/RelayConnectorBuilderExtensions.cs
new file mode 100644
index 000000000..bc53bc0e2
--- /dev/null
+++ b/src/Thinktecture.Relay.Connector/DependencyInjection/RelayConnectorBuilderExtensions.cs
@@ -0,0 +1,55 @@
+using Thinktecture.Relay.Acknowledgement;
+using Thinktecture.Relay.Connector.DependencyInjection;
+using Thinktecture.Relay.Connector.Targets;
+using Thinktecture.Relay.Transport;
+
+// ReSharper disable once CheckNamespace; (extension methods on IServiceCollection namespace)
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for the .
+///
+public static class RelayConnectorBuilderExtensions
+{
+ ///
+ /// Adds the .
+ ///
+ /// The default target key is "$ping".
+ /// The .
+ /// The target key for the .
+ /// The type of request.
+ /// The type of response.
+ /// The type of acknowledge.
+ /// The .
+ public static IRelayConnectorBuilder AddPingTarget(this IRelayConnectorBuilder builder, string targetKey = "$ping")
+ where TRequest : IClientRequest
+ where TResponse : ITargetResponse, new()
+ where TAcknowledge : IAcknowledgeRequest
+ {
+ builder.AddTarget>(targetKey);
+
+ return builder;
+ }
+
+ ///
+ /// Adds the .
+ ///
+ /// The default target key is "$echo" and should only be added for debugging purposes.
+ /// The .
+ /// The target key for the .
+ /// The type of request.
+ /// The type of response.
+ /// The type of acknowledge.
+ /// The .
+ public static IRelayConnectorBuilder AddEchoTarget(this IRelayConnectorBuilder builder, string targetKey = "$echo")
+ where TRequest : IClientRequest
+ where TResponse : ITargetResponse, new()
+ where TAcknowledge : IAcknowledgeRequest
+ {
+ builder.AddTarget>(targetKey);
+
+ return builder;
+ }
+}
diff --git a/src/Thinktecture.Relay.Connector/Targets/EchoTarget.cs b/src/Thinktecture.Relay.Connector/Targets/EchoTarget.cs
new file mode 100644
index 000000000..230ed0128
--- /dev/null
+++ b/src/Thinktecture.Relay.Connector/Targets/EchoTarget.cs
@@ -0,0 +1,83 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Thinktecture.Relay.Transport;
+
+namespace Thinktecture.Relay.Connector.Targets;
+
+///
+public class EchoTarget : IRelayTargetFunc
+ where TRequest : IClientRequest
+ where TResponse : ITargetResponse, new()
+{
+ private class CopyStream(Stream source, long length) : Stream
+ {
+ private long _length = Math.Max(0, length);
+
+ public override void Flush()
+ => throw new NotImplementedException();
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (Position == _length) return 0;
+
+ var read = 0;
+
+ while (read < count && Position < _length)
+ {
+ var remaining = Math.Min(_length - Position, count);
+ var available = source.Read(buffer, offset, (int)remaining);
+ if (available == 0)
+ {
+ source.Position = 0;
+ }
+
+ offset += available;
+ read += available;
+ count -= available;
+
+ Position += available;
+ }
+
+ return read;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException();
+
+ public override void SetLength(long value)
+ => _length = value;
+
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => false;
+
+ public override long Length => _length;
+
+ public override long Position { get; set; }
+ }
+
+ ///
+ public Task HandleAsync(TRequest request, CancellationToken cancellationToken = default)
+ {
+ if (!request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) || request.BodyContent is null)
+ return Task.FromResult(request.CreateResponse(HttpStatusCode.NoContent));
+
+ var result = request.CreateResponse(HttpStatusCode.OK);
+
+ result.BodyContent = int.TryParse(request.Url, out var size)
+ ? new CopyStream(request.BodyContent, size)
+ : request.BodyContent;
+ result.BodySize = result.BodyContent.Length;
+ result.BodyContent.Position = 0;
+
+ return Task.FromResult(result);
+ }
+}
diff --git a/src/Thinktecture.Relay.Connector/Targets/PingTarget.cs b/src/Thinktecture.Relay.Connector/Targets/PingTarget.cs
new file mode 100644
index 000000000..f22ddd602
--- /dev/null
+++ b/src/Thinktecture.Relay.Connector/Targets/PingTarget.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Thinktecture.Relay.Transport;
+
+namespace Thinktecture.Relay.Connector.Targets;
+
+///
+public class PingTarget : IRelayTargetFunc
+ where TRequest : IClientRequest
+ where TResponse : ITargetResponse, new()
+{
+ ///
+ public Task HandleAsync(TRequest request, CancellationToken cancellationToken = default)
+ {
+ if (!request.HttpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase))
+ return Task.FromResult(request.CreateResponse(HttpStatusCode.NotFound));
+
+ var result = request.CreateResponse(HttpStatusCode.OK);
+
+ result.HttpHeaders = new Dictionary()
+ {
+ { "Content-Type", ["text/plain"] },
+ };
+ result.BodyContent = new MemoryStream("PONG"u8.ToArray());
+ result.BodySize = result.BodyContent.Length;
+
+ return Task.FromResult(result);
+ }
+}
diff --git a/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs b/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs
index bf467410f..dc1ac0d51 100644
--- a/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs
+++ b/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs
@@ -25,6 +25,8 @@ public static void ConfigureServices(HostBuilderContext hostBuilderContext, ISer
services
.AddRelayConnector(options => configuration.GetSection("RelayConnector").Bind(options))
.AddSignalRConnectorTransport()
+ .AddPingTarget()
+ .AddEchoTarget()
.AddTarget("inprocfunc")
.AddTarget("inprocaction");