From 6b67c83c0e05640ccef7069a1fe0c427a774fc78 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 07:24:45 -0500 Subject: [PATCH] feat(batch3): verify client proxy protocol feature group --- .../ClientConnection.ProxyProto.cs | 65 ++++++++++ .../ZB.MOM.NatsNet.Server/ClientConnection.cs | 5 +- .../Protocol/ProxyProtocolTests.cs | 119 ++++++++++++++++++ porting.db | Bin 6356992 -> 6361088 bytes reports/current.md | 8 +- 5 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.ProxyProto.cs 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 21c728ded7b331121b362522c258a7e050a2602a..6364f8dbe0be6d18858035712366d71f9039e3e4 100644 GIT binary patch delta 3442 zcmcK6{cjU>90%~b>%-dh*3z=U80~iBLB_Cby>{!^pwAu8gN>o%Vc4geJ=oUK-V~zI z76v0=%$AsZjfn~P2M}kJBZL_El^+!ONfSa)`N@D85^-u`e7DMNHTR?a;njTJyNA!G z-@98cpP$m?FHdP^R_pu}rMJ#WhdimS|pYNMu(Ddi}c zy}|U!&#yA(W<$TZi7Ku%Vec}R8FLf4eUI_WPwp~&jUInrcByxI{R0LxpamW1p$ZIO zglc*H1C#SCvHhiSko=E!E6K;%7ILdur_=GCZg(g7EY8j>**uXb-~35}eNLVYEnhrZ zOYS7u&81UVN~f4=m18or7HVYHh@nW?8Z=#Tv-{ zB=%VxK;}l+n>4YlFgKEi)2y|)=RarvcffF({egV*9_u8=G;1a?kzKd+@b^X5L6dv8 zO%5`#w#rJrJI?Y;wolicV4()ga@`5b<0(_sdFm#^GL~N~kI2J*roK@h(+}t)`c8d| zzCq9GFY4dcj~gVzh+)XE%dl1E=PiC(E^Fq0I3nWC3X~;(S@Mw&pIe^N z*T}`+ET`UOqQ+~~JIa9(qzNU3krGM@Aq^@iSa#BAS&EdUa9IkKB|12LX2Al>zzQ~4 z4z*ARD_|wm!zyTi)nEsQd}g7s>JSNigiG+LSF0sA`kU62me^dn{)M(Fua%;`HDuWz zY%O`~K$CHr9cTg$nqdvBg>|qVUV;tK0vn+fHo?o_gw4I;9Q(o{Kq;bj7xDz%#8PNV}g{< zq;lMw;^-)s5V&|MC8Rj)FQvsCC*dHzo$qwE^G?2tbMf6g-|h0CX;h_Yq|!|zDow+S zO~s0Q)38cYa&oP!wSub~Qt267{Gye)q(POYfl4G@ zWHK|_!^J04qJ&Q=<4lgmbGhPAQjUE6d#s_6cXxX{m}9Tn91)^6M|%b4aJ-mfkJ=n$ zHhW|>nb~Nyk^E_w^*;;K;jO?x%EIhc8z`i<+a#{Hk+wRB}DGtIgq4o5Nj+2S-!u(_J-+ zqWdlv*0P#`=<*PIa{78mUJtRE_qnH34!&boTruHOV(o_yb{}w#I%XKDZ6X=fNDY&z z%B$w{$rLaLm9oiH@?YmiXH9km^63Xw=hC!HheE*)k~ zOkN#fZH%6*up6}0I7&q9sdB?tcFV&oIrfvi9Eil74yUKbiF*gEv&x#dlcr|zBS(+L zi~w?Qd{r{B=RGm8@|0RT6(iMpGAa8u?=vuAhoUHDafdEH4zER6FUXePi(O|o%NZl}uWNtE&p|I*R3u%3)$fKILaLZ6!^fCH! z4iEH)wG1*lg`j0|;h~r9IFq_43p>OkMm@FD{+_A$^HzPQcul8t|KCDTv`dsw_mJC^ z7rR6rW@qX7)LraOrx*Lg{PEBCX73l^1xse{w?8IL{g85x3vPe0-_ffUo@41zy;ZN* z7wJJgR~IreY}YdSIAR}^LnHPj&2pbr%8oldT^(IcoSRhtmkY4otgUo`%EPbe8q zS6J9fz8@l!PHyz4^@>(M6v7NBf?_Cv0F=T^2*NBVgV`_#%Ao>6Fc;>*d{_XLun-nO z7^A4McEC>91-oI76s^&*YpU&PB=>OCFWDPj