// Copyright 2025 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. // // Adapted from server/client_proxyproto_test.go in the NATS server Go source. using System.Buffers.Binary; 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; /// /// Unit tests for , , /// and . /// Adapted from server/client_proxyproto_test.go. /// [Collection("ProxyProtocol")] public sealed class ProxyProtocolTests { // ========================================================================= // Test helpers — mirrors Go helper functions // ========================================================================= /// /// Builds a valid PROXY protocol v2 binary header. /// Mirrors Go buildProxyV2Header. /// private static byte[] BuildProxyV2Header( string srcIP, string dstIP, ushort srcPort, ushort dstPort, byte family) { using var buf = new MemoryStream(); // 12-byte signature const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; foreach (char c in v2Sig) buf.WriteByte((byte)c); // ver/cmd: version 2 (0x20) | PROXY command (0x01) buf.WriteByte(0x21); // proxyProtoV2Ver | proxyProtoCmdProxy // fam/proto buf.WriteByte((byte)(family | 0x01)); // family | ProtoStream var src = IPAddress.Parse(srcIP); var dst = IPAddress.Parse(dstIP); byte[] addrData; if (family == 0x10) // FamilyInet { addrData = new byte[12]; // 4+4+2+2 src.GetAddressBytes().CopyTo(addrData, 0); dst.GetAddressBytes().CopyTo(addrData, 4); BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(8, 2), srcPort); BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(10, 2), dstPort); } else if (family == 0x20) // FamilyInet6 { addrData = new byte[36]; // 16+16+2+2 src.GetAddressBytes().CopyTo(addrData, 0); dst.GetAddressBytes().CopyTo(addrData, 16); BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(32, 2), srcPort); BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(34, 2), dstPort); } else { throw new ArgumentException($"unsupported family: {family}"); } // addr-len (big-endian 2 bytes) var lenBytes = new byte[2]; BinaryPrimitives.WriteUInt16BigEndian(lenBytes, (ushort)addrData.Length); buf.Write(lenBytes); buf.Write(addrData); return buf.ToArray(); } /// /// Builds a PROXY protocol v2 LOCAL command header. /// Mirrors Go buildProxyV2LocalHeader. /// private static byte[] BuildProxyV2LocalHeader() { using var buf = new MemoryStream(); const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; foreach (char c in v2Sig) buf.WriteByte((byte)c); buf.WriteByte(0x20); // proxyProtoV2Ver | proxyProtoCmdLocal buf.WriteByte(0x00); // FamilyUnspec | ProtoUnspec buf.WriteByte(0); buf.WriteByte(0); return buf.ToArray(); } /// /// Builds a PROXY protocol v1 text header. /// Mirrors Go buildProxyV1Header. /// private static byte[] BuildProxyV1Header( string protocol, string srcIP, string dstIP, ushort srcPort, ushort dstPort) { string line; if (protocol == "UNKNOWN") line = "PROXY UNKNOWN\r\n"; else line = $"PROXY {protocol} {srcIP} {dstIP} {srcPort} {dstPort}\r\n"; return System.Text.Encoding.ASCII.GetBytes(line); } // ========================================================================= // PROXY Protocol v2 Parse Tests // ========================================================================= /// Test ID 159 — TestClientProxyProtoV2ParseIPv4 [Fact] public void ProxyProtoV2_ParseIPv4_ReturnsCorrectAddresses() { var header = BuildProxyV2Header("192.168.1.50", "10.0.0.1", 12345, 4222, 0x10); using var stream = new MemoryStream(header); var addr = ProxyProtocolParser.ReadProxyProtoV2Header(stream); addr.ShouldNotBeNull(); addr!.SrcIp.ToString().ShouldBe("192.168.1.50"); addr.SrcPort.ShouldBe((ushort)12345); addr.DstIp.ToString().ShouldBe("10.0.0.1"); addr.DstPort.ShouldBe((ushort)4222); addr.String().ShouldBe("192.168.1.50:12345"); addr.Network().ShouldBe("tcp4"); } /// Test ID 160 — TestClientProxyProtoV2ParseIPv6 [Fact] public void ProxyProtoV2_ParseIPv6_ReturnsCorrectAddresses() { var header = BuildProxyV2Header("2001:db8::1", "2001:db8::2", 54321, 4222, 0x20); using var stream = new MemoryStream(header); var addr = ProxyProtocolParser.ReadProxyProtoV2Header(stream); addr.ShouldNotBeNull(); addr!.SrcIp.ToString().ShouldBe("2001:db8::1"); addr.SrcPort.ShouldBe((ushort)54321); addr.DstIp.ToString().ShouldBe("2001:db8::2"); addr.DstPort.ShouldBe((ushort)4222); addr.String().ShouldBe("[2001:db8::1]:54321"); addr.Network().ShouldBe("tcp6"); } /// Test ID 161 — TestClientProxyProtoV2ParseLocalCommand [Fact] public void ProxyProtoV2_LocalCommand_ReturnsNull() { var header = BuildProxyV2LocalHeader(); using var stream = new MemoryStream(header); var addr = ProxyProtocolParser.ReadProxyProtoV2Header(stream); addr.ShouldBeNull(); } /// Test ID 162 — TestClientProxyProtoV2InvalidSignature [Fact] public void ProxyProtoV2_InvalidSignature_ThrowsInvalidData() { var header = new byte[16]; System.Text.Encoding.ASCII.GetBytes("INVALID_SIG_").CopyTo(header, 0); header[12] = 0x20; header[13] = 0x11; header[14] = 0x00; header[15] = 0x0C; using var stream = new MemoryStream(header); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoV2Header(stream)); } /// Test ID 163 — TestClientProxyProtoV2InvalidVersion [Fact] public void ProxyProtoV2_InvalidVersion_ThrowsInvalidData() { using var buf = new MemoryStream(); const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; foreach (char c in v2Sig) buf.WriteByte((byte)c); buf.WriteByte(0x10 | 0x01); // version 1 instead of 2 — invalid buf.WriteByte(0x10 | 0x01); // FamilyInet | ProtoStream buf.WriteByte(0); buf.WriteByte(0); using var stream = new MemoryStream(buf.ToArray()); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoV2Header(stream)); } /// Test ID 164 — TestClientProxyProtoV2UnsupportedFamily [Fact] public void ProxyProtoV2_UnixSocketFamily_ThrowsUnsupported() { using var buf = new MemoryStream(); const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; foreach (char c in v2Sig) buf.WriteByte((byte)c); buf.WriteByte(0x21); // v2 ver | proxy cmd buf.WriteByte(0x30 | 0x01); // FamilyUnix | ProtoStream buf.WriteByte(0); buf.WriteByte(0); using var stream = new MemoryStream(buf.ToArray()); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoV2Header(stream)); } /// Test ID 165 — TestClientProxyProtoV2UnsupportedProtocol [Fact] public void ProxyProtoV2_UdpProtocol_ThrowsUnsupported() { using var buf = new MemoryStream(); const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; foreach (char c in v2Sig) buf.WriteByte((byte)c); buf.WriteByte(0x21); // v2 ver | proxy cmd buf.WriteByte(0x10 | 0x02); // FamilyInet | ProtoDatagram (UDP) buf.WriteByte(0); buf.WriteByte(12); // addr len = 12 using var stream = new MemoryStream(buf.ToArray()); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoV2Header(stream)); } /// Test ID 166 — TestClientProxyProtoV2TruncatedHeader [Fact] public void ProxyProtoV2_TruncatedHeader_ThrowsIOException() { var fullHeader = BuildProxyV2Header("192.168.1.50", "10.0.0.1", 12345, 4222, 0x10); // Only provide first 10 bytes — header is 16 bytes minimum using var stream = new MemoryStream(fullHeader[..10]); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoV2Header(stream)); } /// Test ID 167 — TestClientProxyProtoV2ShortAddressData [Fact] public void ProxyProtoV2_ShortAddressData_ThrowsIOException() { using var buf = new MemoryStream(); const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; foreach (char c in v2Sig) buf.WriteByte((byte)c); buf.WriteByte(0x21); // v2 ver | proxy cmd buf.WriteByte(0x10 | 0x01); // FamilyInet | ProtoStream buf.WriteByte(0); buf.WriteByte(12); // addr len = 12 but only 5 bytes follow buf.Write(new byte[] { 1, 2, 3, 4, 5 }); // only 5 bytes using var stream = new MemoryStream(buf.ToArray()); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoV2Header(stream)); } /// Test ID 168 — TestProxyConnRemoteAddr [Fact] public void ProxyConn_RemoteAddr_ReturnsProxiedAddress() { var proxyAddr = new ProxyProtocolAddress( IPAddress.Parse("10.0.0.50"), 12345, IPAddress.Parse("10.0.0.1"), 4222); using var inner = new MemoryStream(); var wrapped = new ProxyProtocolConnection(inner, proxyAddr); wrapped.RemoteAddress.String().ShouldBe("10.0.0.50:12345"); } // ========================================================================= // PROXY Protocol v1 Parse Tests // ========================================================================= /// Test ID 171 — TestClientProxyProtoV1ParseTCP4 [Fact] public void ProxyProtoV1_ParseTCP4_ReturnsCorrectAddresses() { var header = BuildProxyV1Header("TCP4", "192.168.1.50", "10.0.0.1", 12345, 4222); using var stream = new MemoryStream(header); var addr = ProxyProtocolParser.ReadProxyProtoHeader(stream); addr.ShouldNotBeNull(); addr!.SrcIp.ToString().ShouldBe("192.168.1.50"); addr.SrcPort.ShouldBe((ushort)12345); addr.DstIp.ToString().ShouldBe("10.0.0.1"); addr.DstPort.ShouldBe((ushort)4222); } /// Test ID 172 — TestClientProxyProtoV1ParseTCP6 [Fact] public void ProxyProtoV1_ParseTCP6_ReturnsCorrectAddresses() { var header = BuildProxyV1Header("TCP6", "2001:db8::1", "2001:db8::2", 54321, 4222); using var stream = new MemoryStream(header); var addr = ProxyProtocolParser.ReadProxyProtoHeader(stream); addr.ShouldNotBeNull(); addr!.SrcIp.ToString().ShouldBe("2001:db8::1"); addr.SrcPort.ShouldBe((ushort)54321); addr.DstIp.ToString().ShouldBe("2001:db8::2"); addr.DstPort.ShouldBe((ushort)4222); } /// Test ID 173 — TestClientProxyProtoV1ParseUnknown [Fact] public void ProxyProtoV1_UnknownProtocol_ReturnsNull() { var header = BuildProxyV1Header("UNKNOWN", "", "", 0, 0); using var stream = new MemoryStream(header); var addr = ProxyProtocolParser.ReadProxyProtoHeader(stream); addr.ShouldBeNull(); } /// Test ID 174 — TestClientProxyProtoV1InvalidFormat [Fact] public void ProxyProtoV1_MissingFields_ThrowsInvalidData() { var header = System.Text.Encoding.ASCII.GetBytes("PROXY TCP4 192.168.1.1\r\n"); using var stream = new MemoryStream(header); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoHeader(stream)); } /// Test ID 175 — TestClientProxyProtoV1LineTooLong [Fact] public void ProxyProtoV1_LineTooLong_ThrowsInvalidData() { var longIp = new string('1', 120); var line = $"PROXY TCP4 {longIp} 10.0.0.1 12345 443\r\n"; var header = System.Text.Encoding.ASCII.GetBytes(line); using var stream = new MemoryStream(header); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoHeader(stream)); } /// Test ID 176 — TestClientProxyProtoV1InvalidIP [Fact] public void ProxyProtoV1_InvalidIPAddress_ThrowsInvalidData() { var header = System.Text.Encoding.ASCII.GetBytes( "PROXY TCP4 not.an.ip.addr 10.0.0.1 12345 443\r\n"); using var stream = new MemoryStream(header); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoHeader(stream)); } /// Test ID 177 — TestClientProxyProtoV1MismatchedProtocol [Fact] public void ProxyProtoV1_TCP4WithIPv6Address_ThrowsInvalidData() { // TCP4 with IPv6 address var header = BuildProxyV1Header("TCP4", "2001:db8::1", "2001:db8::2", 12345, 443); using var stream = new MemoryStream(header); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoHeader(stream)); // TCP6 with IPv4 address var header2 = BuildProxyV1Header("TCP6", "192.168.1.1", "10.0.0.1", 12345, 443); using var stream2 = new MemoryStream(header2); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoHeader(stream2)); } /// Test ID 178 — TestClientProxyProtoV1InvalidPort [Fact] public void ProxyProtoV1_InvalidPort_ThrowsException() { var header = System.Text.Encoding.ASCII.GetBytes( "PROXY TCP4 192.168.1.1 10.0.0.1 99999 443\r\n"); using var stream = new MemoryStream(header); Should.Throw(() => ProxyProtocolParser.ReadProxyProtoHeader(stream)); } // ========================================================================= // Mixed Protocol Version Tests // ========================================================================= /// Test ID 180 — TestClientProxyProtoVersionDetection [Fact] public void ProxyProto_AutoDetect_HandlesV1AndV2() { // v1 detection var v1Header = BuildProxyV1Header("TCP4", "192.168.1.1", "10.0.0.1", 12345, 443); using var stream1 = new MemoryStream(v1Header); var addr1 = ProxyProtocolParser.ReadProxyProtoHeader(stream1); addr1.ShouldNotBeNull(); addr1!.SrcIp.ToString().ShouldBe("192.168.1.1"); // v2 detection var v2Header = BuildProxyV2Header("192.168.1.2", "10.0.0.1", 54321, 443, 0x10); using var stream2 = new MemoryStream(v2Header); var addr2 = ProxyProtocolParser.ReadProxyProtoHeader(stream2); addr2.ShouldNotBeNull(); addr2!.SrcIp.ToString().ShouldBe("192.168.1.2"); } /// Test ID 181 — TestClientProxyProtoUnrecognizedVersion [Fact] public void ProxyProto_UnrecognizedFormat_ThrowsInvalidData() { var header = System.Text.Encoding.ASCII.GetBytes("HELLO WORLD\r\n"); using var stream = new MemoryStream(header); 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); } }