diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.ProxyProto.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.ProxyProto.cs new file mode 100644 index 0000000..9f6f07d --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.ProxyProto.cs @@ -0,0 +1,65 @@ +// Copyright 2012-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Net; +using ZB.MOM.NatsNet.Server.Protocol; + +namespace ZB.MOM.NatsNet.Server; + +/// +/// Client-side PROXY protocol compatibility surface for Batch 3 mappings. +/// +public sealed partial class ClientConnection +{ + private IPEndPoint? _proxyRemoteEndPoint; + + /// + /// Returns the proxied remote endpoint when available, otherwise socket remote endpoint. + /// Mirrors Go proxyConn.RemoteAddr(). + /// + public EndPoint? RemoteAddr() + { + lock (_mu) { return _proxyRemoteEndPoint ?? GetRemoteEndPoint(); } + } + + internal void SetProxyRemoteAddress(ProxyProtocolAddress? address) + { + lock (_mu) + { + _proxyRemoteEndPoint = address is null + ? null + : new IPEndPoint(address.SrcIp, address.SrcPort); + } + } + + internal static (int version, byte[] header) DetectProxyProtoVersion(Stream conn) => + ProxyProtocolParser.DetectVersion(conn); + + internal static ProxyProtocolAddress? ReadProxyProtoV1Header(Stream conn) => + ProxyProtocolParser.ReadV1Header(conn); + + internal static ProxyProtocolAddress? ReadProxyProtoHeader(Stream conn) => + ProxyProtocolParser.ReadProxyProtoHeader(conn); + + internal static ProxyProtocolAddress? ReadProxyProtoV2Header(Stream conn) => + ProxyProtocolParser.ReadProxyProtoV2Header(conn); + + internal static ProxyProtocolAddress? ParseProxyProtoV2Header(Stream conn, byte[] header) => + ProxyProtocolParser.ParseV2Header(conn, header.AsSpan()); + + internal static ProxyProtocolAddress ParseIPv4Addr(Stream conn, ushort addrLen) => + ProxyProtocolParser.ParseIPv4Addr(conn, addrLen); + + internal static ProxyProtocolAddress ParseIPv6Addr(Stream conn, ushort addrLen) => + ProxyProtocolParser.ParseIPv6Addr(conn, addrLen); +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs index f35f978..8f677f6 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs @@ -374,10 +374,7 @@ public sealed partial class ClientConnection /// Returns the remote network address of the connection, or null. /// Mirrors Go client.RemoteAddress(). /// - public EndPoint? RemoteAddress() - { - lock (_mu) { return GetRemoteEndPoint(); } - } + public EndPoint? RemoteAddress() => RemoteAddr(); private EndPoint? GetRemoteEndPoint() { diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProxyProtocolTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProxyProtocolTests.cs index 086644a..bb65920 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProxyProtocolTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProxyProtocolTests.cs @@ -18,6 +18,8 @@ using System.Net; using System.Net.Sockets; using Shouldly; using Xunit; +using ZB.MOM.NatsNet.Server; +using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Protocol; namespace ZB.MOM.NatsNet.Server.Tests.Protocol; @@ -427,4 +429,121 @@ public sealed class ProxyProtocolTests Should.Throw(() => ProxyProtocolParser.ReadProxyProtoHeader(stream)); } + + [Fact] + public void DetectProxyProtoVersion_WhenV1Header_ReturnsVersionAndPrefix() + { + var header = BuildProxyV1Header("TCP4", "127.0.0.1", "10.0.0.1", 12345, 4222); + using var stream = new MemoryStream(header); + + var (version, firstBytes) = ClientConnection.DetectProxyProtoVersion(stream); + + version.ShouldBe(1); + System.Text.Encoding.ASCII.GetString(firstBytes).ShouldBe("PROXY "); + } + + [Fact] + public void ReadProxyProtoV1Header_WhenPrefixConsumed_ParsesV1Payload() + { + var header = BuildProxyV1Header("TCP4", "192.168.1.50", "10.0.0.1", 12345, 4222); + using var stream = new MemoryStream(header[6..]); + + var addr = ClientConnection.ReadProxyProtoV1Header(stream); + + addr.ShouldNotBeNull(); + addr!.SrcIp.ToString().ShouldBe("192.168.1.50"); + addr.SrcPort.ShouldBe((ushort)12345); + } + + [Fact] + public void ReadProxyProtoHeader_WhenV2Header_ParsesAddress() + { + var header = BuildProxyV2Header("192.168.1.60", "10.0.0.1", 2222, 4222, 0x10); + using var stream = new MemoryStream(header); + + var addr = ClientConnection.ReadProxyProtoHeader(stream); + + addr.ShouldNotBeNull(); + addr!.SrcIp.ToString().ShouldBe("192.168.1.60"); + addr.SrcPort.ShouldBe((ushort)2222); + } + + [Fact] + public void ReadProxyProtoV2Header_WhenValidHeader_ParsesAddress() + { + var header = BuildProxyV2Header("2001:db8::11", "2001:db8::22", 3001, 4222, 0x20); + using var stream = new MemoryStream(header); + + var addr = ClientConnection.ReadProxyProtoV2Header(stream); + + addr.ShouldNotBeNull(); + addr!.SrcIp.ToString().ShouldBe("2001:db8::11"); + addr.SrcPort.ShouldBe((ushort)3001); + } + + [Fact] + public void ParseProxyProtoV2Header_WhenIPv4Family_ParsesAddress() + { + var header = new byte[] { 0x21, 0x11, 0x00, 0x0C }; + var addrData = new byte[12]; + IPAddress.Parse("172.16.1.10").GetAddressBytes().CopyTo(addrData, 0); + IPAddress.Parse("172.16.1.1").GetAddressBytes().CopyTo(addrData, 4); + BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(8, 2), 5000); + BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(10, 2), 4222); + using var stream = new MemoryStream(addrData); + + var addr = ClientConnection.ParseProxyProtoV2Header(stream, header); + + addr.ShouldNotBeNull(); + addr!.SrcIp.ToString().ShouldBe("172.16.1.10"); + addr.SrcPort.ShouldBe((ushort)5000); + } + + [Fact] + public void ParseIPv4Addr_WhenValidPayload_ParsesAddress() + { + var addrData = new byte[12]; + IPAddress.Parse("192.0.2.20").GetAddressBytes().CopyTo(addrData, 0); + IPAddress.Parse("192.0.2.10").GetAddressBytes().CopyTo(addrData, 4); + BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(8, 2), 7000); + BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(10, 2), 4222); + using var stream = new MemoryStream(addrData); + + var addr = ClientConnection.ParseIPv4Addr(stream, (ushort)addrData.Length); + + addr.SrcIp.ToString().ShouldBe("192.0.2.20"); + addr.SrcPort.ShouldBe((ushort)7000); + } + + [Fact] + public void ParseIPv6Addr_WhenValidPayload_ParsesAddress() + { + var addrData = new byte[36]; + IPAddress.Parse("2001:db8::20").GetAddressBytes().CopyTo(addrData, 0); + IPAddress.Parse("2001:db8::10").GetAddressBytes().CopyTo(addrData, 16); + BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(32, 2), 8000); + BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(34, 2), 4222); + using var stream = new MemoryStream(addrData); + + var addr = ClientConnection.ParseIPv6Addr(stream, (ushort)addrData.Length); + + addr.SrcIp.ToString().ShouldBe("2001:db8::20"); + addr.SrcPort.ShouldBe((ushort)8000); + } + + [Fact] + public void RemoteAddr_WhenProxyAddressPresent_ReturnsProxyEndpoint() + { + var client = new ClientConnection(ClientKind.Client); + client.SetProxyRemoteAddress(new ProxyProtocolAddress( + IPAddress.Parse("203.0.113.10"), 4444, IPAddress.Parse("10.0.0.1"), 4222)); + + var remote = client.RemoteAddr(); + + remote.ShouldNotBeNull(); + remote.ShouldBeOfType(); + var endpoint = (IPEndPoint)remote; + endpoint.Address.ToString().ShouldBe("203.0.113.10"); + endpoint.Port.ShouldBe(4444); + } } diff --git a/porting.db b/porting.db index 21c728d..6364f8d 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index 3a94d48..92f8af9 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 12:18:22 UTC +Generated: 2026-02-28 12:24:46 UTC ## Modules (12 total) @@ -12,10 +12,10 @@ Generated: 2026-02-28 12:18:22 UTC | Status | Count | |--------|-------| -| deferred | 2359 | +| deferred | 2351 | | n_a | 24 | | stub | 1 | -| verified | 1289 | +| verified | 1297 | ## Unit Tests (3257 total) @@ -34,4 +34,4 @@ Generated: 2026-02-28 12:18:22 UTC ## Overall Progress -**2491/6942 items complete (35.9%)** +**2499/6942 items complete (36.0%)**