From e18e18d52899be940a2295b1a4b06d7a82265a2e Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Mon, 27 Oct 2025 15:33:54 +0100 Subject: [PATCH 01/31] RAV-2958 - Initial Proxy Protocol processing --- .github/workflows/pull_requests.yml | 43 +++++ LICENSE | 28 ++-- README.md | 65 +++++++- pom.xml | 121 ++++++++++++++ proxy-socket-core/pom.xml | 31 ++++ .../proxysocket/core/ProxyAddressCache.java | 20 +++ .../core/ProxyProtocolException.java | 17 ++ .../core/ProxyProtocolMetricsListener.java | 19 +++ .../core/ProxyProtocolParseException.java | 17 ++ .../cache/ConcurrentMapProxyAddressCache.java | 49 ++++++ .../core/cache/NoOpProxyAddressCache.java | 28 ++++ .../proxysocket/core/v2/ProxyHeader.java | 53 ++++++ .../core/v2/ProxyProtocolV2Decoder.java | 150 +++++++++++++++++ .../core/v2/ProxyProtocolV2Encoder.java | 122 ++++++++++++++ .../airvantage/proxysocket/core/v2/Tlv.java | 30 ++++ .../v2/ProxyProtocolV2DecoderVectorsTest.java | 126 +++++++++++++++ .../core/v2/ProxyProtocolV2Test.java | 152 ++++++++++++++++++ 17 files changed, 1060 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/pull_requests.yml create mode 100755 pom.xml create mode 100644 proxy-socket-core/pom.xml create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java create mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java create mode 100644 proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderVectorsTest.java create mode 100644 proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml new file mode 100644 index 0000000..ef3f799 --- /dev/null +++ b/.github/workflows/pull_requests.yml @@ -0,0 +1,43 @@ +name: Pull Request +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Test (Java ${{ matrix.java }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java: [ '11', '17', '21' ] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: maven + + - name: Build and test + run: | + mvn -B -e -DskipITs=true -DskipIT=true -DskipITsTests=true -DskipIntegrationTests=true -DskipDocker=true -DskipNative=true -DskipExamples=true --fail-at-end clean verify + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports-java-${{ matrix.java }} + path: | + ./**/target/surefire-reports/** + ./**/target/failsafe-reports/** diff --git a/LICENSE b/LICENSE index f288702..0f59d12 100644 --- a/LICENSE +++ b/LICENSE @@ -1,14 +1,24 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +MIT License - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Copyright (c) 2025 Semtech - Preamble +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - The GNU General Public License is a free, copyleft license for -software and other kinds of works. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, @@ -332,7 +342,7 @@ for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and -protocols for communication across the network. +sockets for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly diff --git a/README.md b/README.md index 9325cb3..8ec1823 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,63 @@ -# proxy-socket-java -Java Library to handle ProxyProtocol v2 on standard java sockets +Proxy Socket Java (UDP + TCP, Java 17) +======================================= + +Overview +-------- +Library providing HAProxy Proxy Protocol v2 support for UDP and TCP. Multi-module layout: + +- proxy-socket-core: zero dependencies, parser, models, interfaces +- proxy-socket-udp: DatagramSocket wrapper +- proxy-socket-tcp: ServerSocket/Socket wrappers +- proxy-socket-guava: optional Guava-based cache +- proxy-socket-examples: runnable samples + +Quick start (UDP) +----------------- +```java +var socket = new net.airvantage.proxysocket.udp.ProxyDatagramSocket.Builder() + .maxEntries(10_000) + .ttl(java.time.Duration.ofMinutes(5)) + .metrics(new MyMetrics()) + .build(); +socket.bind(new java.net.InetSocketAddress(9999)); +var buf = new byte[2048]; +var packet = new java.net.DatagramPacket(buf, buf.length); +socket.receive(packet); // header stripped, source set to real client +socket.send(packet); // destination rewritten to LB if cached +``` + +Quick start (TCP) +----------------- +```java +try (var server = new net.airvantage.proxysocket.tcp.ProxyServerSocket(9998)) { + for (;;) { + var s = (net.airvantage.proxysocket.tcp.ProxySocket) server.accept(); + var header = s.getHeader(); + // header.getSourceAddress() is the real client address + } +} +``` + +License +------- +MIT License © 2025 Semtech. See `LICENSE`. + +Metrics hook +------------ +Implement `net.airvantage.proxysocket.core.ProxySocketMetricsListener` and pass it via UDP builder or TCP server ctor. + +Thread safety +------------- +- UDP/TCP wrappers follow JDK `DatagramSocket`/`ServerSocket`/`Socket` thread-safety; caches and listeners must be thread-safe. +- Core parser is stateless and thread-safe. + +Configuration +------------- +- UDP cache defaults: 10k entries, 5 min TTL if Guava present; otherwise concurrent map (no TTL). +- TCP: blocking header read on accept with configurable timeout. + +Examples +-------- +See `proxy-socket-examples` module: `UdpEchoWithProxyProtocol`, `TcpEchoWithProxyProtocol`. + + diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..e75902f --- /dev/null +++ b/pom.xml @@ -0,0 +1,121 @@ + + + + 4.0.0 + + net.airvantage + proxysocket-java + 1.0.0-SNAPSHOT + pom + + ProxyProtocol Java implementation. + + + + aws-release + AWS Release Repository + s3://av-maven-repo/release + + + aws-snapshot + AWS Snapshot Repository + s3://av-maven-repo/snapshot + + + + + + + + 5.10.3 + 9.4.57.v20241219 + 1.12.261 + 1.14.4 + 2.19.1 + 2.0.16 + 1.3.15 + 4.1.1 + 3.9.1 + + + + + + + + + + + proxy-socket-core + + + + + + + com.github.seahen + maven-s3-wagon + 1.3.3 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + UTF-8 + 17 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + false + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + UTF-8 + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.10 + + true + true + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar + + + + + + + + diff --git a/proxy-socket-core/pom.xml b/proxy-socket-core/pom.xml new file mode 100644 index 0000000..de118de --- /dev/null +++ b/proxy-socket-core/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + net.airvantage + proxysocket-java + 1.0.0-SNAPSHOT + + proxy-socket-core + Proxy Protocol - Core + jar + + + 17 + + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java new file mode 100644 index 0000000..cfa6149 --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java @@ -0,0 +1,20 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core; + +import java.net.InetSocketAddress; + +/** + * Thread-safe cache abstraction mapping real client addresses to proxy/load-balancer addresses. + */ +public interface ProxyAddressCache { + void put(InetSocketAddress clientAddr, InetSocketAddress proxyAddr); + InetSocketAddress get(InetSocketAddress clientAddr); + void invalidate(InetSocketAddress clientAddr); + void clear(); +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java new file mode 100644 index 0000000..a23d246 --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java @@ -0,0 +1,17 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core; + +public class ProxyProtocolException extends Exception { + public ProxyProtocolException(String message) { + super(message); + } + public ProxyProtocolException(String message, Throwable cause) { + super(message, cause); + } +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java new file mode 100644 index 0000000..6ff4225 --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java @@ -0,0 +1,19 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core; + +import net.airvantage.proxysocket.core.v2.ProxyHeader; +import java.net.InetSocketAddress; + +/** + * Metrics/observability callbacks for Proxy Protocol processing. + * Implementations must be thread-safe. + */ +public interface ProxyProtocolMetricsListener { + default void onHeaderParsed(ProxyHeader header) {} + default void onParseError(Exception e) {} + default void onCacheHit(InetSocketAddress client) {} + default void onCacheMiss(InetSocketAddress client) {} +} \ No newline at end of file diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java new file mode 100644 index 0000000..1faee06 --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java @@ -0,0 +1,17 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core; + +public final class ProxyProtocolParseException extends ProxyProtocolException { + public ProxyProtocolParseException(String message) { + super(message); + } + public ProxyProtocolParseException(String message, Throwable cause) { + super(message, cause); + } +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java new file mode 100644 index 0000000..918353d --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java @@ -0,0 +1,49 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core.cache; + +import net.airvantage.proxysocket.core.ProxyAddressCache; +import java.net.InetSocketAddress; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Simple thread-safe cache backed by {@link ConcurrentHashMap}. + */ +public final class ConcurrentMapProxyAddressCache implements ProxyAddressCache { + private final ConcurrentMap map = new ConcurrentHashMap<>(); + + @Override + public void put(InetSocketAddress clientAddr, InetSocketAddress proxyAddr) { + if (clientAddr == null || proxyAddr == null) { + return; + } + map.put(clientAddr, proxyAddr); + } + + @Override + public InetSocketAddress get(InetSocketAddress clientAddr) { + if (clientAddr == null) { + return null; + } + return map.get(clientAddr); + } + + @Override + public void invalidate(InetSocketAddress clientAddr) { + if (clientAddr == null) { + return; + } + map.remove(clientAddr); + } + + @Override + public void clear() { + map.clear(); + } +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java new file mode 100644 index 0000000..9a5e504 --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java @@ -0,0 +1,28 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core.cache; + +import net.airvantage.proxysocket.core.ProxyAddressCache; +import java.net.InetSocketAddress; + +/** + * No-op implementation useful when state should not be retained. + */ +public final class NoOpProxyAddressCache implements ProxyAddressCache { + @Override + public void put(InetSocketAddress clientAddr, InetSocketAddress proxyAddr) { } + + @Override + public InetSocketAddress get(InetSocketAddress clientAddr) { return null; } + + @Override + public void invalidate(InetSocketAddress clientAddr) { } + + @Override + public void clear() { } +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java new file mode 100644 index 0000000..8efcabf --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java @@ -0,0 +1,53 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core.v2; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; + +public final class ProxyHeader { + public enum Command { LOCAL, PROXY } + public enum AddressFamily { INET4, INET6, UNIX, UNSPEC } + public enum TransportProtocol { STREAM, DGRAM, UNSPEC } + + private final Command command; + private final AddressFamily family; + private final TransportProtocol socket; + private final InetSocketAddress sourceAddress; + private final InetSocketAddress destinationAddress; + private final List tlvs; + private final int headerLength; + + public ProxyHeader(Command command, + AddressFamily family, + TransportProtocol socket, + InetSocketAddress sourceAddress, + InetSocketAddress destinationAddress, + List tlvs, + int headerLength) { + this.command = command; + this.family = family; + this.socket = socket; + this.sourceAddress = sourceAddress; + this.destinationAddress = destinationAddress; + this.tlvs = tlvs == null ? List.of() : List.copyOf(tlvs); + this.headerLength = headerLength; + } + + public Command getCommand() { return command; } + public AddressFamily getFamily() { return family; } + public TransportProtocol getProtocol() { return socket; } + public InetSocketAddress getSourceAddress() { return sourceAddress; } + public InetSocketAddress getDestinationAddress() { return destinationAddress; } + public List getTlvs() { return Collections.unmodifiableList(tlvs); } + public int getHeaderLength() { return headerLength; } + + public boolean isLocal() { return command == Command.LOCAL; } + public boolean isProxy() { return command == Command.PROXY; } +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java new file mode 100644 index 0000000..485a8a6 --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -0,0 +1,150 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core.v2; + +import net.airvantage.proxysocket.core.ProxyProtocolParseException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Dependency-free Proxy Protocol v2 utilities (validate/parse/build). + */ +public final class ProxyProtocolV2Decoder { + private ProxyProtocolV2Decoder() {} + + private static final byte[] SIG = "\r\n\r\n\0\r\nQUIT\n".getBytes(StandardCharsets.ISO_8859_1); + private static final int IPV4_ADDR_LEN = 4; + private static final int IPV6_ADDR_LEN = 16; + private static final int UNIX_ADDR_LEN = 216; + private static final int PORT_LEN = 2; + private static final int TLV_HEADER_LEN = 3; + + public static ProxyHeader parse(byte[] data, int offset, int length) throws ProxyProtocolParseException { + if (data == null || offset < 0 || length < 0) throw new ProxyProtocolParseException("Null data"); + if ((length+offset) > data.length) throw new ProxyProtocolParseException("Invalid offset/length"); + + int end = offset + length; + if (SIG.length + 4 > length) throw new ProxyProtocolParseException("Insufficient data for header"); + + for (int i = 0; i < SIG.length; i++) { + if (data[offset + i] != SIG[i]) { + throw new ProxyProtocolParseException("Invalid signature"); + } + } + + int pos = offset + SIG.length; + + int verCmd = data[pos++] & 0xFF; // version/command + int version = (verCmd >> 4) & 0x0F; + if (version != 2) throw new ProxyProtocolParseException("Invalid version"); + int cmd = verCmd & 0x0F; + ProxyHeader.Command command = cmd == 0x00 ? ProxyHeader.Command.LOCAL : ProxyHeader.Command.PROXY; + + int famProto = data[pos++] & 0xFF; + int fam = famProto & 0xF0; + int proto = famProto & 0x0F; + + ProxyHeader.AddressFamily af = switch (fam) { + case 0x10 -> ProxyHeader.AddressFamily.INET4; + case 0x20 -> ProxyHeader.AddressFamily.INET6; + case 0x30 -> ProxyHeader.AddressFamily.UNIX; + default -> ProxyHeader.AddressFamily.UNSPEC; + }; + ProxyHeader.TransportProtocol tp = switch (proto) { + case 0x01 -> ProxyHeader.TransportProtocol.STREAM; // TCP + case 0x02 -> ProxyHeader.TransportProtocol.DGRAM; // UDP + default -> ProxyHeader.TransportProtocol.UNSPEC; + }; + + int variableLength = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + + int headerLen = SIG.length + 4 + variableLength; + if (headerLen > length) throw new ProxyProtocolParseException("Insufficient data for header"); + + int addrStart = pos; + InetSocketAddress src = null; + InetSocketAddress dst = null; + + if (command == ProxyHeader.Command.PROXY) { + if (af == ProxyHeader.AddressFamily.INET4 && tp != ProxyHeader.TransportProtocol.UNSPEC) { + if (variableLength < 2*(IPV4_ADDR_LEN + PORT_LEN)) { + throw new ProxyProtocolParseException("Truncated IPv4 address block in header"); + } + + // Extract source and destination addresses + InetAddress s; + InetAddress d; + try { + s = InetAddress.getByAddress(new byte[]{data[pos++], data[pos++], data[pos++], data[pos++]}); + d = InetAddress.getByAddress(new byte[]{data[pos++], data[pos++], data[pos++], data[pos++]}); + } catch (UnknownHostException e) { + throw new ProxyProtocolParseException("Invalid IPv4 address in header", e); + } + // Extract source and destination ports + int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + src = new InetSocketAddress(s, sp); + dst = new InetSocketAddress(d, dp); + + } else if (af == ProxyHeader.AddressFamily.INET6 && tp != ProxyHeader.TransportProtocol.UNSPEC) { + if (variableLength < 2*(IPV6_ADDR_LEN + PORT_LEN)) { + throw new ProxyProtocolParseException("Truncated IPv6 address block in header"); + } + + // Extract source and destination addresses + InetAddress s; + InetAddress d; + byte[] sb = new byte[16]; + byte[] db = new byte[16]; + System.arraycopy(data, pos, sb, 0, IPV6_ADDR_LEN); + System.arraycopy(data, pos+IPV6_ADDR_LEN, db, 0, IPV6_ADDR_LEN); + try { + s = InetAddress.getByAddress(sb); + d = InetAddress.getByAddress(db); + } catch (UnknownHostException e) { + throw new ProxyProtocolParseException("Invalid IPv6 address in header", e); + } + + pos += 2*IPV6_ADDR_LEN; + // Extract source and destination ports + int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + src = new InetSocketAddress(s, sp); + dst = new InetSocketAddress(d, dp); + + } else if (af == ProxyHeader.AddressFamily.UNIX) { + // 108 + 108 bytes path + if (variableLength < 2*UNIX_ADDR_LEN) { + throw new ProxyProtocolParseException("Truncated UNIX address block in header"); + } + pos += 2*UNIX_ADDR_LEN; + throw new ProxyProtocolParseException("UNIX Address Processing not implemented"); + } + } + + int consumed = pos - addrStart; + int tlvLen = Math.max(0, variableLength - consumed); + List tlvs = new ArrayList<>(); + int tlvPos = pos; + int tlvEnd = tlvPos + tlvLen; + while (tlvPos + TLV_HEADER_LEN <= tlvEnd) { + int type = data[tlvPos++] & 0xFF; + int len = ((data[tlvPos++] & 0xFF) << 8) | (data[tlvPos++] & 0xFF); + + if (tlvPos + len > tlvEnd) break; + tlvs.add(new Tlv(type, data, tlvPos, len)); + tlvPos += len; + } + + return new ProxyHeader(command, af, tp, src, dst, tlvs, headerLen); + } +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java new file mode 100644 index 0000000..eef9097 --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java @@ -0,0 +1,122 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core.v2; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Minimal builder for generating Proxy Protocol v2 headers for tests. + */ +public final class ProxyProtocolV2Encoder { + private static final byte[] SIG = "\r\n\r\n\0\r\nQUIT\n".getBytes(StandardCharsets.ISO_8859_1); + + private ProxyHeader.Command command = ProxyHeader.Command.PROXY; + private ProxyHeader.AddressFamily family = ProxyHeader.AddressFamily.INET4; + private ProxyHeader.TransportProtocol socket = ProxyHeader.TransportProtocol.STREAM; + private InetSocketAddress source; + private InetSocketAddress destination; + private final List tlvs = new ArrayList<>(); + + public ProxyProtocolV2Encoder command(ProxyHeader.Command c) { this.command = c; return this; } + public ProxyProtocolV2Encoder family(ProxyHeader.AddressFamily f) { this.family = f; return this; } + public ProxyProtocolV2Encoder socket(ProxyHeader.TransportProtocol p) { this.socket = p; return this; } + public ProxyProtocolV2Encoder source(InetSocketAddress s) { this.source = s; return this; } + public ProxyProtocolV2Encoder destination(InetSocketAddress d) { this.destination = d; return this; } + public ProxyProtocolV2Encoder addTlv(int type, byte[] value) { this.tlvs.add(new Tlv(type, value)); return this; } + + public byte[] build() { + byte verCmd = (byte) ((2 << 4) | (command == ProxyHeader.Command.PROXY ? 0x01 : 0x00)); + int fam = switch (family) { case INET4 -> 0x10; case INET6 -> 0x20; case UNIX -> 0x30; default -> 0x00; }; + int proto = switch (socket) { case STREAM -> 0x01; case DGRAM -> 0x02; default -> 0x00; }; + byte famProto = (byte) (fam | proto); + + byte[] addr = buildAddr(); + byte[] tlvBytes = buildTlvs(); + int addrLen = addr.length + tlvBytes.length; + byte[] out = new byte[16 + addrLen]; + int p = 0; + System.arraycopy(SIG, 0, out, p, SIG.length); p += SIG.length; + out[p++] = verCmd; + out[p++] = famProto; + out[p++] = (byte) ((addrLen >>> 8) & 0xFF); + out[p++] = (byte) (addrLen & 0xFF); + System.arraycopy(addr, 0, out, p, addr.length); p += addr.length; + System.arraycopy(tlvBytes, 0, out, p, tlvBytes.length); + return out; + } + + private byte[] buildAddr() { + if (command == ProxyHeader.Command.LOCAL) return new byte[0]; + if (family == ProxyHeader.AddressFamily.INET4 && (socket == ProxyHeader.TransportProtocol.STREAM || socket == ProxyHeader.TransportProtocol.DGRAM)) { + byte[] b = new byte[12]; + writeIPv4PortPair(b); + return b; + } + if (family == ProxyHeader.AddressFamily.INET6 && (socket == ProxyHeader.TransportProtocol.STREAM || socket == ProxyHeader.TransportProtocol.DGRAM)) { + byte[] b = new byte[36]; + writeIPv6PortPair(b); + return b; + } + return new byte[0]; + } + + private void writeIPv4PortPair(byte[] b) { + byte[] src = source == null ? new byte[4] : source.getAddress().getAddress(); + byte[] dst = destination == null ? new byte[4] : destination.getAddress().getAddress(); + System.arraycopy(src, 0, b, 0, 4); + System.arraycopy(dst, 0, b, 4, 4); + int sp = source == null ? 0 : source.getPort(); + int dp = destination == null ? 0 : destination.getPort(); + b[8] = (byte) ((sp >>> 8) & 0xFF); b[9] = (byte) (sp & 0xFF); + b[10] = (byte) ((dp >>> 8) & 0xFF); b[11] = (byte) (dp & 0xFF); + } + + private void writeIPv6PortPair(byte[] b) { + byte[] src = source == null ? new byte[16] : toIPv6Bytes(source.getAddress()); + byte[] dst = destination == null ? new byte[16] : toIPv6Bytes(destination.getAddress()); + System.arraycopy(src, 0, b, 0, 16); + System.arraycopy(dst, 0, b, 16, 16); + int sp = source == null ? 0 : source.getPort(); + int dp = destination == null ? 0 : destination.getPort(); + b[32] = (byte) ((sp >>> 8) & 0xFF); b[33] = (byte) (sp & 0xFF); + b[34] = (byte) ((dp >>> 8) & 0xFF); b[35] = (byte) (dp & 0xFF); + } + + private static byte[] toIPv6Bytes(InetAddress addr) { + byte[] a = addr.getAddress(); + if (a.length == 16) return a; + // IPv4-mapped IPv6 ::ffff:a.b.c.d + byte[] v6 = new byte[16]; + v6[10] = (byte) 0xFF; v6[11] = (byte) 0xFF; + System.arraycopy(a, 0, v6, 12, 4); + return v6; + } + + private byte[] buildTlvs() { + int total = 0; + for (Tlv t : tlvs) total += 3 + (t.getValue() == null ? 0 : t.getValue().length); + byte[] buf = new byte[total]; + int p = 0; + for (Tlv t : tlvs) { + byte[] v = t.getValue(); + int len = v == null ? 0 : v.length; + buf[p++] = (byte) (t.getType() & 0xFF); + buf[p++] = (byte) ((len >>> 8) & 0xFF); + buf[p++] = (byte) (len & 0xFF); + if (len > 0) { + System.arraycopy(v, 0, buf, p, len); + p += len; + } + } + return buf; + } +} + + + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java new file mode 100644 index 0000000..e072a4f --- /dev/null +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java @@ -0,0 +1,30 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core.v2; + +import java.util.Arrays; + +public final class Tlv { + private final int type; + private final byte[] value; + + public Tlv(int type, byte[] data, int offset, int length) { + this.type = type; + this.value = Arrays.copyOfRange(data, offset, offset + length); + } + + public int getType() { return type; } + public byte[] getValue() { return value.clone(); } + + @Override + public String toString() { + int show = Math.min(value.length, 16); + byte[] head = Arrays.copyOf(value, show); + return "Tlv{" + "type=" + type + ", len=" + value.length + ", head=" + Arrays.toString(head) + (value.length > show ? ", ..." : "") + '}'; + } +} + + + diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderVectorsTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderVectorsTest.java new file mode 100644 index 0000000..ad3d5df --- /dev/null +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderVectorsTest.java @@ -0,0 +1,126 @@ +package net.airvantage.proxysocket.core.v2; + +import net.airvantage.proxysocket.core.ProxyProtocolParseException; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class ProxyProtocolV2DecoderVectorsTest { + private static byte[] sig() { + return "\r\n\r\n\0\r\nQUIT\n".getBytes(StandardCharsets.ISO_8859_1); + } + + @Test + void decodeIPv4Tcp_fixedVector() throws Exception { + byte[] s = sig(); + byte verCmd = (byte) 0x21; // v2, PROXY + byte famProto = (byte) 0x11; // INET4 + STREAM + byte[] payload = new byte[]{ + // len = 12 + 0x00, 0x0C, + // src 127.0.0.1 + 0x7F, 0x00, 0x00, 0x01, + // dst 127.0.0.2 + 0x7F, 0x00, 0x00, 0x02, + // sport 12345 (0x3039) + 0x30, 0x39, + // dport 443 (0x01BB) + 0x01, (byte) 0xBB + }; + byte[] h = new byte[s.length + 4 + 12]; + int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + h[p++] = verCmd; h[p++] = famProto; + System.arraycopy(payload, 0, h, p, payload.length); + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + assertEquals(ProxyHeader.Command.PROXY, parsed.getCommand()); + assertEquals(ProxyHeader.AddressFamily.INET4, parsed.getFamily()); + assertEquals(ProxyHeader.TransportProtocol.STREAM, parsed.getProtocol()); + assertEquals(12345, parsed.getSourceAddress().getPort()); + assertEquals(443, parsed.getDestinationAddress().getPort()); + assertEquals(h.length, parsed.getHeaderLength()); + } + + @Test + void decodeIPv6Udp_fixedVector() throws Exception { + byte[] s = sig(); + byte verCmd = (byte) 0x21; // v2, PROXY + byte famProto = (byte) 0x22; // INET6 + DGRAM + + byte[] addr = new byte[2 + 36]; + // len = 36 + addr[0] = 0x00; addr[1] = 0x24; + int q = 2; + // src ::1 + for (int i = 0; i < 15; i++) addr[q + i] = 0x00; addr[q + 15] = 0x01; q += 16; + // dst ::2 + for (int i = 0; i < 15; i++) addr[q + i] = 0x00; addr[q + 15] = 0x02; q += 16; + // sport 1000 (0x03E8) + addr[q++] = 0x03; addr[q++] = (byte) 0xE8; + // dport 2000 (0x07D0) + addr[q++] = 0x07; addr[q++] = (byte) 0xD0; + + byte[] h = new byte[s.length + 4 + 36]; + int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + h[p++] = verCmd; h[p++] = famProto; + System.arraycopy(addr, 0, h, p, addr.length); + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + assertEquals(ProxyHeader.AddressFamily.INET6, parsed.getFamily()); + assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); + assertEquals(1000, parsed.getSourceAddress().getPort()); + assertEquals(2000, parsed.getDestinationAddress().getPort()); + assertEquals(h.length, parsed.getHeaderLength()); + } + + @Test + void decodeLocal_fixedVector() throws Exception { + byte[] s = sig(); + byte verCmd = (byte) 0x20; // v2, LOCAL + byte famProto = (byte) 0x00; // UNSPEC + byte[] h = new byte[s.length + 4]; + int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + h[p++] = verCmd; h[p++] = famProto; h[p++] = 0x00; h[p++] = 0x00; + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + assertTrue(parsed.isLocal()); + assertNull(parsed.getSourceAddress()); + assertEquals(16, parsed.getHeaderLength()); + } + + @Test + void decodeUnspecWithTlvOnly_fixedVector() throws Exception { + byte[] s = sig(); + byte verCmd = (byte) 0x21; // v2, PROXY + byte famProto = (byte) 0x00; // UNSPEC + UNSPEC + + // TLV: type=0xEE, len=3, value=10 20 30 + byte[] tlv = new byte[]{ (byte) 0xEE, 0x00, 0x03, 0x10, 0x20, 0x30 }; + // variable length = 0 (addr) + TLV len (6) + byte[] h = new byte[s.length + 4 + tlv.length]; + int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + h[p++] = verCmd; h[p++] = famProto; + h[p++] = 0x00; h[p++] = (byte) tlv.length; + System.arraycopy(tlv, 0, h, p, tlv.length); + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + assertEquals(ProxyHeader.AddressFamily.UNSPEC, parsed.getFamily()); + assertNotNull(parsed.getTlvs()); + assertEquals(1, parsed.getTlvs().size()); + assertArrayEquals(new byte[]{0x10, 0x20, 0x30}, parsed.getTlvs().get(0).getValue()); + } + + @Test + void invalidSignature_throws() { + byte[] s = sig(); + s[0] ^= 0x01; // corrupt + byte[] h = new byte[s.length + 4]; + int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + h[p++] = 0x20; h[p++] = 0x00; h[p++] = 0x00; h[p++] = 0x00; + assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length)); + } +} + + diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java new file mode 100644 index 0000000..a0975c5 --- /dev/null +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -0,0 +1,152 @@ +package net.airvantage.proxysocket.core.v2; + +import net.airvantage.proxysocket.core.ProxyProtocolParseException; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class ProxyProtocolV2Test { + @Test + void validateRejectsNonHeader() { + byte[] data = "hello".getBytes(StandardCharsets.UTF_8); + assertEquals(0, ProxyProtocolV2.validate(data, 0, data.length)); + } + + @Test + void parseIPv4Tcp() throws Exception { + var header = new ProxyProtocolV2Builder() + .family(ProxyHeader.AddressFamily.INET4) + .socket(ProxyHeader.TransportProtocol.STREAM) + .source(new InetSocketAddress("127.0.0.1", 12345)) + .destination(new InetSocketAddress("127.0.0.2", 443)) + .build(); + int len = ProxyProtocolV2.validate(header, 0, header.length); + assertTrue(len > 0); + ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + assertEquals(ProxyHeader.Command.PROXY, parsed.getCommand()); + assertEquals(ProxyHeader.AddressFamily.INET4, parsed.getFamily()); + assertEquals(ProxyHeader.TransportProtocol.STREAM, parsed.getProtocol()); + assertEquals(12345, parsed.getSourceAddress().getPort()); + assertEquals(443, parsed.getDestinationAddress().getPort()); + } + + @Test + void parseIPv6UdpWithTlv() throws Exception { + var header = new ProxyProtocolV2Builder() + .family(ProxyHeader.AddressFamily.INET6) + .socket(ProxyHeader.TransportProtocol.DGRAM) + .source(new InetSocketAddress("::1", 1000)) + .destination(new InetSocketAddress("::2", 2000)) + .addTlv(0x01, new byte[]{0x41, 0x42}) + .build(); + int len = ProxyProtocolV2.validate(header, 0, header.length); + assertTrue(len > 0); + ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + assertEquals(ProxyHeader.AddressFamily.INET6, parsed.getFamily()); + assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); + assertEquals(1, parsed.getTlvs().size()); + assertArrayEquals(new byte[]{0x41, 0x42}, parsed.getTlvs().get(0).getValue()); + } + + @Test + void parseLocal() throws Exception { + var header = new ProxyProtocolV2Builder() + .command(ProxyHeader.Command.LOCAL) + .build(); + int len = ProxyProtocolV2.validate(header, 0, header.length); + assertTrue(len > 0); + ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + assertTrue(parsed.isLocal()); + assertNull(parsed.getSourceAddress()); + } + + @Test + void invalidVersionThrows() { + byte[] h = new ProxyProtocolV2Builder().build(); + // corrupt version nibble + h[12] = (byte) ((1 << 4) | 0x01); + assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2.parse(h, 0, h.length)); + } + + @Test + void truncatedHeaderInvalid() { + byte[] h = new ProxyProtocolV2Builder().build(); + assertEquals(0, ProxyProtocolV2.validate(h, 0, 10)); + } + + @Test + void tlvLengthOverrunIgnored() throws Exception { + // Build header then break TLV length to exceed buffer; parser should stop TLV loop gracefully + var builder = new ProxyProtocolV2Builder() + .addTlv(0x01, new byte[]{0x01}); + byte[] h = builder.build(); + // Set TLV length to something large + int tlvStart = h.length - 3 - 1; // type(1) + len(2) + value(1) + h[tlvStart + 1] = (byte) 0x7F; + h[tlvStart + 2] = (byte) 0x7F; + ProxyHeader parsed = ProxyProtocolV2.parse(h, 0, h.length); + assertNotNull(parsed); + } + + @Test + void parseIPv4Udp() throws Exception { + var header = new ProxyProtocolV2Builder() + .family(ProxyHeader.AddressFamily.INET4) + .socket(ProxyHeader.TransportProtocol.DGRAM) + .source(new InetSocketAddress("127.0.0.1", 1111)) + .destination(new InetSocketAddress("127.0.0.2", 2222)) + .build(); + assertTrue(ProxyProtocolV2.validate(header, 0, header.length) > 0); + ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); + assertEquals(1111, parsed.getSourceAddress().getPort()); + assertEquals(2222, parsed.getDestinationAddress().getPort()); + } + + @Test + void localHeaderHasLength16() throws Exception { + var header = new ProxyProtocolV2Builder() + .command(ProxyHeader.Command.LOCAL) + .build(); + assertEquals(16, header.length); + assertEquals(16, ProxyProtocolV2.validate(header, 0, header.length)); + } + + @Test + void invalidSignatureRejected() { + byte[] h = new ProxyProtocolV2Builder().build(); + h[0] ^= 0x01; // corrupt signature + assertEquals(0, ProxyProtocolV2.validate(h, 0, h.length)); + } + + @Test + void addressLengthBeyondBufferInvalid() { + byte[] h = new ProxyProtocolV2Builder().build(); + // Bump declared length by one without providing data + int lenPos = 14; + int len = ((h[lenPos] & 0xFF) << 8) | (h[lenPos + 1] & 0xFF); + len += 1; + h[lenPos] = (byte) ((len >>> 8) & 0xFF); + h[lenPos + 1] = (byte) (len & 0xFF); + assertEquals(0, ProxyProtocolV2.validate(h, 0, h.length)); + } + + @Test + void proxyUnspecWithTlvOnly() throws Exception { + var header = new ProxyProtocolV2Builder() + .family(ProxyHeader.AddressFamily.UNSPEC) + .socket(ProxyHeader.TransportProtocol.UNSPEC) + .addTlv(0xEE, new byte[]{0x10, 0x20, 0x30}) + .build(); + assertTrue(ProxyProtocolV2.validate(header, 0, header.length) > 0); + ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + assertEquals(ProxyHeader.AddressFamily.UNSPEC, parsed.getFamily()); + assertEquals(1, parsed.getTlvs().size()); + assertArrayEquals(new byte[]{0x10, 0x20, 0x30}, parsed.getTlvs().get(0).getValue()); + } +} + + From 887da06a97a877e3557565885e5b612b8eb935f4 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Mon, 27 Oct 2025 15:36:04 +0100 Subject: [PATCH 02/31] add spec link : --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8ec1823..356a6a5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Library providing HAProxy Proxy Protocol v2 support for UDP and TCP. Multi-modul - proxy-socket-guava: optional Guava-based cache - proxy-socket-examples: runnable samples +Reference: [HAProxy Proxy Protocol Specifications](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) + Quick start (UDP) ----------------- ```java From a34382d9201c612e20849200328544047f18095e Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Mon, 27 Oct 2025 15:38:03 +0100 Subject: [PATCH 03/31] fix tls constructor --- .../airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java index eef9097..f19eb12 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java @@ -28,7 +28,7 @@ public final class ProxyProtocolV2Encoder { public ProxyProtocolV2Encoder socket(ProxyHeader.TransportProtocol p) { this.socket = p; return this; } public ProxyProtocolV2Encoder source(InetSocketAddress s) { this.source = s; return this; } public ProxyProtocolV2Encoder destination(InetSocketAddress d) { this.destination = d; return this; } - public ProxyProtocolV2Encoder addTlv(int type, byte[] value) { this.tlvs.add(new Tlv(type, value)); return this; } + public ProxyProtocolV2Encoder addTlv(int type, byte[] value) { this.tlvs.add(new Tlv(type, value, 0, value.length)); return this; } public byte[] build() { byte verCmd = (byte) ((2 << 4) | (command == ProxyHeader.Command.PROXY ? 0x01 : 0x00)); From b3e53890e79925c35c058349f926a016a6fdeff2 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Mon, 27 Oct 2025 16:02:41 +0100 Subject: [PATCH 04/31] cleanup test --- pom.xml | 39 ---------- ...t.java => ProxyProtocolV2DecoderTest.java} | 61 ++++++++++------ .../core/v2/ProxyProtocolV2Test.java | 73 ++++++------------- 3 files changed, 61 insertions(+), 112 deletions(-) rename proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/{ProxyProtocolV2DecoderVectorsTest.java => ProxyProtocolV2DecoderTest.java} (68%) diff --git a/pom.xml b/pom.xml index e75902f..c4ef13f 100755 --- a/pom.xml +++ b/pom.xml @@ -12,58 +12,19 @@ ProxyProtocol Java implementation. - - - aws-release - AWS Release Repository - s3://av-maven-repo/release - - - aws-snapshot - AWS Snapshot Repository - s3://av-maven-repo/snapshot - - - - 5.10.3 - 9.4.57.v20241219 - 1.12.261 - 1.14.4 - 2.19.1 - 2.0.16 - 1.3.15 - 4.1.1 - 3.9.1 - - - - - - proxy-socket-core - - - - com.github.seahen - maven-s3-wagon - 1.3.3 - - - - - org.apache.maven.plugins maven-compiler-plugin diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderVectorsTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java similarity index 68% rename from proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderVectorsTest.java rename to proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java index ad3d5df..0a25710 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderVectorsTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java @@ -7,14 +7,17 @@ import static org.junit.jupiter.api.Assertions.*; -class ProxyProtocolV2DecoderVectorsTest { - private static byte[] sig() { - return "\r\n\r\n\0\r\nQUIT\n".getBytes(StandardCharsets.ISO_8859_1); +class ProxyProtocolV2DecoderTest { + private static final byte[] SIG = "\r\n\r\n\0\r\nQUIT\n".getBytes(StandardCharsets.ISO_8859_1); + + @Test + void validateRejectsNonHeader() { + byte[] data = "hello".getBytes(StandardCharsets.UTF_8); + assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(data, 0, data.length)); } @Test - void decodeIPv4Tcp_fixedVector() throws Exception { - byte[] s = sig(); + void decodeIPv4Tcp() throws Exception { byte verCmd = (byte) 0x21; // v2, PROXY byte famProto = (byte) 0x11; // INET4 + STREAM byte[] payload = new byte[]{ @@ -29,8 +32,8 @@ void decodeIPv4Tcp_fixedVector() throws Exception { // dport 443 (0x01BB) 0x01, (byte) 0xBB }; - byte[] h = new byte[s.length + 4 + 12]; - int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + byte[] h = new byte[SIG.length + 4 + 12]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; h[p++] = verCmd; h[p++] = famProto; System.arraycopy(payload, 0, h, p, payload.length); @@ -44,8 +47,7 @@ void decodeIPv4Tcp_fixedVector() throws Exception { } @Test - void decodeIPv6Udp_fixedVector() throws Exception { - byte[] s = sig(); + void decodeIPv6Udp() throws Exception { byte verCmd = (byte) 0x21; // v2, PROXY byte famProto = (byte) 0x22; // INET6 + DGRAM @@ -62,8 +64,8 @@ void decodeIPv6Udp_fixedVector() throws Exception { // dport 2000 (0x07D0) addr[q++] = 0x07; addr[q++] = (byte) 0xD0; - byte[] h = new byte[s.length + 4 + 36]; - int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + byte[] h = new byte[SIG.length + 4 + 36]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; h[p++] = verCmd; h[p++] = famProto; System.arraycopy(addr, 0, h, p, addr.length); @@ -76,12 +78,11 @@ void decodeIPv6Udp_fixedVector() throws Exception { } @Test - void decodeLocal_fixedVector() throws Exception { - byte[] s = sig(); + void decodeLocal() throws Exception { byte verCmd = (byte) 0x20; // v2, LOCAL byte famProto = (byte) 0x00; // UNSPEC - byte[] h = new byte[s.length + 4]; - int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + byte[] h = new byte[SIG.length + 4]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; h[p++] = verCmd; h[p++] = famProto; h[p++] = 0x00; h[p++] = 0x00; ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); @@ -91,16 +92,15 @@ void decodeLocal_fixedVector() throws Exception { } @Test - void decodeUnspecWithTlvOnly_fixedVector() throws Exception { - byte[] s = sig(); + void decodeUnspecWithTlvOnly() throws Exception { byte verCmd = (byte) 0x21; // v2, PROXY byte famProto = (byte) 0x00; // UNSPEC + UNSPEC // TLV: type=0xEE, len=3, value=10 20 30 byte[] tlv = new byte[]{ (byte) 0xEE, 0x00, 0x03, 0x10, 0x20, 0x30 }; // variable length = 0 (addr) + TLV len (6) - byte[] h = new byte[s.length + 4 + tlv.length]; - int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + byte[] h = new byte[SIG.length + 4 + tlv.length]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; h[p++] = verCmd; h[p++] = famProto; h[p++] = 0x00; h[p++] = (byte) tlv.length; System.arraycopy(tlv, 0, h, p, tlv.length); @@ -113,14 +113,27 @@ void decodeUnspecWithTlvOnly_fixedVector() throws Exception { } @Test - void invalidSignature_throws() { - byte[] s = sig(); - s[0] ^= 0x01; // corrupt - byte[] h = new byte[s.length + 4]; - int p = 0; System.arraycopy(s, 0, h, p, s.length); p += s.length; + void invalidSignature() throws Exception { + byte[] h = new byte[SIG.length + 4]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; + h[0] ^= 0x01; // corrupt h[p++] = 0x20; h[p++] = 0x00; h[p++] = 0x00; h[p++] = 0x00; assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length)); } + + + @Test + void invalidVersion() throws Exception { + byte verCmd = (byte) 0x31; // v3, LOCAL + byte famProto = (byte) 0x00; // UNSPEC + UNSPEC + byte[] h = new byte[SIG.length + 4]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; + h[p++] = verCmd; h[p++] = famProto; + h[p++] = 0x00; h[p++] = 0x00; + + assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length)); + } + } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java index a0975c5..4d820fa 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -9,23 +9,16 @@ import static org.junit.jupiter.api.Assertions.*; class ProxyProtocolV2Test { - @Test - void validateRejectsNonHeader() { - byte[] data = "hello".getBytes(StandardCharsets.UTF_8); - assertEquals(0, ProxyProtocolV2.validate(data, 0, data.length)); - } - @Test void parseIPv4Tcp() throws Exception { - var header = new ProxyProtocolV2Builder() + var header = new ProxyProtocolV2Encoder() .family(ProxyHeader.AddressFamily.INET4) .socket(ProxyHeader.TransportProtocol.STREAM) .source(new InetSocketAddress("127.0.0.1", 12345)) .destination(new InetSocketAddress("127.0.0.2", 443)) .build(); - int len = ProxyProtocolV2.validate(header, 0, header.length); - assertTrue(len > 0); - ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertEquals(ProxyHeader.Command.PROXY, parsed.getCommand()); assertEquals(ProxyHeader.AddressFamily.INET4, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.STREAM, parsed.getProtocol()); @@ -35,16 +28,15 @@ void parseIPv4Tcp() throws Exception { @Test void parseIPv6UdpWithTlv() throws Exception { - var header = new ProxyProtocolV2Builder() + var header = new ProxyProtocolV2Encoder() .family(ProxyHeader.AddressFamily.INET6) .socket(ProxyHeader.TransportProtocol.DGRAM) .source(new InetSocketAddress("::1", 1000)) .destination(new InetSocketAddress("::2", 2000)) .addTlv(0x01, new byte[]{0x41, 0x42}) .build(); - int len = ProxyProtocolV2.validate(header, 0, header.length); - assertTrue(len > 0); - ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertEquals(ProxyHeader.AddressFamily.INET6, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); assertEquals(1, parsed.getTlvs().size()); @@ -53,54 +45,41 @@ void parseIPv6UdpWithTlv() throws Exception { @Test void parseLocal() throws Exception { - var header = new ProxyProtocolV2Builder() + var header = new ProxyProtocolV2Encoder() .command(ProxyHeader.Command.LOCAL) .build(); - int len = ProxyProtocolV2.validate(header, 0, header.length); - assertTrue(len > 0); - ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertTrue(parsed.isLocal()); assertNull(parsed.getSourceAddress()); } - @Test - void invalidVersionThrows() { - byte[] h = new ProxyProtocolV2Builder().build(); - // corrupt version nibble - h[12] = (byte) ((1 << 4) | 0x01); - assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2.parse(h, 0, h.length)); - } - - @Test - void truncatedHeaderInvalid() { - byte[] h = new ProxyProtocolV2Builder().build(); - assertEquals(0, ProxyProtocolV2.validate(h, 0, 10)); - } @Test void tlvLengthOverrunIgnored() throws Exception { // Build header then break TLV length to exceed buffer; parser should stop TLV loop gracefully - var builder = new ProxyProtocolV2Builder() + var builder = new ProxyProtocolV2Encoder() .addTlv(0x01, new byte[]{0x01}); byte[] h = builder.build(); // Set TLV length to something large int tlvStart = h.length - 3 - 1; // type(1) + len(2) + value(1) h[tlvStart + 1] = (byte) 0x7F; h[tlvStart + 2] = (byte) 0x7F; - ProxyHeader parsed = ProxyProtocolV2.parse(h, 0, h.length); + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); assertNotNull(parsed); } @Test void parseIPv4Udp() throws Exception { - var header = new ProxyProtocolV2Builder() + var header = new ProxyProtocolV2Encoder() .family(ProxyHeader.AddressFamily.INET4) .socket(ProxyHeader.TransportProtocol.DGRAM) .source(new InetSocketAddress("127.0.0.1", 1111)) .destination(new InetSocketAddress("127.0.0.2", 2222)) .build(); - assertTrue(ProxyProtocolV2.validate(header, 0, header.length) > 0); - ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); assertEquals(1111, parsed.getSourceAddress().getPort()); assertEquals(2222, parsed.getDestinationAddress().getPort()); @@ -108,41 +87,37 @@ void parseIPv4Udp() throws Exception { @Test void localHeaderHasLength16() throws Exception { - var header = new ProxyProtocolV2Builder() + var header = new ProxyProtocolV2Encoder() .command(ProxyHeader.Command.LOCAL) .build(); assertEquals(16, header.length); - assertEquals(16, ProxyProtocolV2.validate(header, 0, header.length)); + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); + assertEquals(16, parsed.getHeaderLength()); } - @Test - void invalidSignatureRejected() { - byte[] h = new ProxyProtocolV2Builder().build(); - h[0] ^= 0x01; // corrupt signature - assertEquals(0, ProxyProtocolV2.validate(h, 0, h.length)); - } + @Test void addressLengthBeyondBufferInvalid() { - byte[] h = new ProxyProtocolV2Builder().build(); + byte[] h = new ProxyProtocolV2Encoder().build(); // Bump declared length by one without providing data int lenPos = 14; int len = ((h[lenPos] & 0xFF) << 8) | (h[lenPos + 1] & 0xFF); len += 1; h[lenPos] = (byte) ((len >>> 8) & 0xFF); h[lenPos + 1] = (byte) (len & 0xFF); - assertEquals(0, ProxyProtocolV2.validate(h, 0, h.length)); + assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length)); } @Test void proxyUnspecWithTlvOnly() throws Exception { - var header = new ProxyProtocolV2Builder() + var header = new ProxyProtocolV2Encoder() .family(ProxyHeader.AddressFamily.UNSPEC) .socket(ProxyHeader.TransportProtocol.UNSPEC) .addTlv(0xEE, new byte[]{0x10, 0x20, 0x30}) .build(); - assertTrue(ProxyProtocolV2.validate(header, 0, header.length) > 0); - ProxyHeader parsed = ProxyProtocolV2.parse(header, 0, header.length); + + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertEquals(ProxyHeader.AddressFamily.UNSPEC, parsed.getFamily()); assertEquals(1, parsed.getTlvs().size()); assertArrayEquals(new byte[]{0x10, 0x20, 0x30}, parsed.getTlvs().get(0).getValue()); From 74755f0504c8697bdc5ab4b43ebdd0d4499dc9b0 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Mon, 27 Oct 2025 16:06:00 +0100 Subject: [PATCH 05/31] only test supported java releases --- .github/workflows/pull_requests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index ef3f799..91151e0 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - java: [ '11', '17', '21' ] + java: [ '17', '21' ] steps: - name: Checkout uses: actions/checkout@v4 From b74f019946c33b1441e01a3c6fb2bca3a78924d2 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Thu, 6 Nov 2025 11:06:56 +0100 Subject: [PATCH 06/31] cleanup end of files spaces --- .../net/airvantage/proxysocket/core/ProxyAddressCache.java | 3 --- .../airvantage/proxysocket/core/ProxyProtocolException.java | 3 --- .../proxysocket/core/ProxyProtocolMetricsListener.java | 2 +- .../proxysocket/core/ProxyProtocolParseException.java | 3 --- .../proxysocket/core/cache/ConcurrentMapProxyAddressCache.java | 3 --- .../proxysocket/core/cache/NoOpProxyAddressCache.java | 3 --- .../java/net/airvantage/proxysocket/core/v2/ProxyHeader.java | 3 --- .../airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java | 3 --- .../src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java | 3 --- 9 files changed, 1 insertion(+), 25 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java index cfa6149..08df66e 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java @@ -15,6 +15,3 @@ public interface ProxyAddressCache { void invalidate(InetSocketAddress clientAddr); void clear(); } - - - diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java index a23d246..8c1ccf9 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java @@ -12,6 +12,3 @@ public ProxyProtocolException(String message, Throwable cause) { super(message, cause); } } - - - diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java index 6ff4225..e6587d8 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java @@ -16,4 +16,4 @@ default void onHeaderParsed(ProxyHeader header) {} default void onParseError(Exception e) {} default void onCacheHit(InetSocketAddress client) {} default void onCacheMiss(InetSocketAddress client) {} -} \ No newline at end of file +} diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java index 1faee06..d6f3e0e 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java @@ -12,6 +12,3 @@ public ProxyProtocolParseException(String message, Throwable cause) { super(message, cause); } } - - - diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java index 918353d..0d55e5e 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java @@ -44,6 +44,3 @@ public void clear() { map.clear(); } } - - - diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java index 9a5e504..d63ab0f 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java @@ -23,6 +23,3 @@ public void invalidate(InetSocketAddress clientAddr) { } @Override public void clear() { } } - - - diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java index 8efcabf..a97522f 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java @@ -48,6 +48,3 @@ public ProxyHeader(Command command, public boolean isLocal() { return command == Command.LOCAL; } public boolean isProxy() { return command == Command.PROXY; } } - - - diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java index f19eb12..2070b2c 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java @@ -117,6 +117,3 @@ private byte[] buildTlvs() { return buf; } } - - - diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java index e072a4f..7a37f9b 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java @@ -25,6 +25,3 @@ public String toString() { return "Tlv{" + "type=" + type + ", len=" + value.length + ", head=" + Arrays.toString(head) + (value.length > show ? ", ..." : "") + '}'; } } - - - From a4751e68b707cd1d6528c6f213c26bf93efacece Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Thu, 6 Nov 2025 11:14:43 +0100 Subject: [PATCH 07/31] add test suite for basic cache --- .../core/cache/NoOpProxyAddressCache.java | 25 --- .../ConcurrentMapProxyAddressCacheTest.java | 198 ++++++++++++++++++ 2 files changed, 198 insertions(+), 25 deletions(-) delete mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java create mode 100644 proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java deleted file mode 100644 index d63ab0f..0000000 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/NoOpProxyAddressCache.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * MIT License - * Copyright (c) 2025 Semtech - */ -package net.airvantage.proxysocket.core.cache; - -import net.airvantage.proxysocket.core.ProxyAddressCache; -import java.net.InetSocketAddress; - -/** - * No-op implementation useful when state should not be retained. - */ -public final class NoOpProxyAddressCache implements ProxyAddressCache { - @Override - public void put(InetSocketAddress clientAddr, InetSocketAddress proxyAddr) { } - - @Override - public InetSocketAddress get(InetSocketAddress clientAddr) { return null; } - - @Override - public void invalidate(InetSocketAddress clientAddr) { } - - @Override - public void clear() { } -} diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java new file mode 100644 index 0000000..27042ce --- /dev/null +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java @@ -0,0 +1,198 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + */ +package net.airvantage.proxysocket.core.cache; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +class ConcurrentMapProxyAddressCacheTest { + private ConcurrentMapProxyAddressCache cache; + private InetSocketAddress clientAddr1; + private InetSocketAddress clientAddr2; + private InetSocketAddress proxyAddr1; + private InetSocketAddress proxyAddr2; + + @BeforeEach + void setUp() { + cache = new ConcurrentMapProxyAddressCache(); + clientAddr1 = new InetSocketAddress("192.168.1.100", 12345); + clientAddr2 = new InetSocketAddress("192.168.1.101", 12346); + proxyAddr1 = new InetSocketAddress("10.0.0.1", 443); + proxyAddr2 = new InetSocketAddress("10.0.0.2", 443); + } + + @Test + void testPutAndGet() { + cache.put(clientAddr1, proxyAddr1); + InetSocketAddress result = cache.get(clientAddr1); + assertEquals(proxyAddr1, result); + } + + @Test + void testGetNonExistentAddress() { + InetSocketAddress result = cache.get(clientAddr1); + assertNull(result); + } + + @Test + void testPutMultipleAddresses() { + cache.put(clientAddr1, proxyAddr1); + cache.put(clientAddr2, proxyAddr2); + + assertEquals(proxyAddr1, cache.get(clientAddr1)); + assertEquals(proxyAddr2, cache.get(clientAddr2)); + } + + @Test + void testPutOverwritesExistingValue() { + cache.put(clientAddr1, proxyAddr1); + cache.put(clientAddr1, proxyAddr2); + + InetSocketAddress result = cache.get(clientAddr1); + assertEquals(proxyAddr2, result); + } + + @Test + void testPutNullClientAddress() { + cache.put(null, proxyAddr1); + // Should not throw exception, just ignore + assertNull(cache.get(clientAddr1)); + } + + @Test + void testPutNullProxyAddress() { + cache.put(clientAddr1, null); + // Should not throw exception, just ignore + assertNull(cache.get(clientAddr1)); + } + + @Test + void testPutBothNull() { + cache.put(null, null); + // Should not throw exception + assertNull(cache.get(clientAddr1)); + } + + @Test + void testGetNullAddress() { + cache.put(clientAddr1, proxyAddr1); + InetSocketAddress result = cache.get(null); + assertNull(result); + } + + @Test + void testInvalidate() { + cache.put(clientAddr1, proxyAddr1); + cache.put(clientAddr2, proxyAddr2); + + cache.invalidate(clientAddr1); + + assertNull(cache.get(clientAddr1)); + assertEquals(proxyAddr2, cache.get(clientAddr2)); + } + + @Test + void testInvalidateNonExistentAddress() { + cache.invalidate(clientAddr1); + // Should not throw exception + assertNull(cache.get(clientAddr1)); + } + + @Test + void testInvalidateNullAddress() { + cache.put(clientAddr1, proxyAddr1); + cache.invalidate(null); + // Should not throw exception, should not affect existing entries + assertEquals(proxyAddr1, cache.get(clientAddr1)); + } + + @Test + void testClear() { + cache.put(clientAddr1, proxyAddr1); + cache.put(clientAddr2, proxyAddr2); + + cache.clear(); + + assertNull(cache.get(clientAddr1)); + assertNull(cache.get(clientAddr2)); + } + + @Test + void testConcurrentPutAndGet() throws InterruptedException { + int threadCount = 10; + int operationsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + executor.submit(() -> { + try { + for (int j = 0; j < operationsPerThread; j++) { + InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + threadId, 10000 + j); + InetSocketAddress proxyAddr = new InetSocketAddress("10.0.0." + threadId, 443); + cache.put(clientAddr, proxyAddr); + InetSocketAddress retrieved = cache.get(clientAddr); + assertNotNull(retrieved); + assertEquals(proxyAddr, retrieved); + } + } finally { + latch.countDown(); + } + }); + } + + assertTrue(latch.await(10, TimeUnit.SECONDS)); + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + void testConcurrentInvalidate() throws InterruptedException { + // Pre-populate cache + for (int i = 0; i < 100; i++) { + InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + i, 10000); + InetSocketAddress proxyAddr = new InetSocketAddress("10.0.0." + i, 443); + cache.put(clientAddr, proxyAddr); + } + + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + executor.submit(() -> { + try { + for (int j = 0; j < 10; j++) { + InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + (threadId * 10 + j), 10000); + cache.invalidate(clientAddr); + } + } finally { + latch.countDown(); + } + }); + } + + assertTrue(latch.await(10, TimeUnit.SECONDS)); + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + + // Verify all addresses were invalidated + for (int i = 0; i < 100; i++) { + InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + i, 10000); + assertNull(cache.get(clientAddr)); + } + } +} + From f260ccd64f81dcd59c5f001fb1e772039467e581 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Thu, 6 Nov 2025 11:22:52 +0100 Subject: [PATCH 08/31] update readme --- README.md | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 356a6a5..b41acc2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -Proxy Socket Java (UDP + TCP, Java 17) -======================================= +# Proxy Socket Java (UDP + TCP, Java 17) + +## Overview -Overview --------- Library providing HAProxy Proxy Protocol v2 support for UDP and TCP. Multi-module layout: - proxy-socket-core: zero dependencies, parser, models, interfaces @@ -13,8 +12,8 @@ Library providing HAProxy Proxy Protocol v2 support for UDP and TCP. Multi-modul Reference: [HAProxy Proxy Protocol Specifications](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) -Quick start (UDP) ------------------ +## Quick start (UDP) + ```java var socket = new net.airvantage.proxysocket.udp.ProxyDatagramSocket.Builder() .maxEntries(10_000) @@ -28,8 +27,8 @@ socket.receive(packet); // header stripped, source set to real client socket.send(packet); // destination rewritten to LB if cached ``` -Quick start (TCP) ------------------ +## Quick start (TCP) + ```java try (var server = new net.airvantage.proxysocket.tcp.ProxyServerSocket(9998)) { for (;;) { @@ -40,26 +39,24 @@ try (var server = new net.airvantage.proxysocket.tcp.ProxyServerSocket(9998)) { } ``` -License -------- +## License + MIT License © 2025 Semtech. See `LICENSE`. -Metrics hook ------------- +## Metrics hook + Implement `net.airvantage.proxysocket.core.ProxySocketMetricsListener` and pass it via UDP builder or TCP server ctor. -Thread safety -------------- +## Thread safety + - UDP/TCP wrappers follow JDK `DatagramSocket`/`ServerSocket`/`Socket` thread-safety; caches and listeners must be thread-safe. - Core parser is stateless and thread-safe. -Configuration -------------- +## Configuration + - UDP cache defaults: 10k entries, 5 min TTL if Guava present; otherwise concurrent map (no TTL). - TCP: blocking header read on accept with configurable timeout. -Examples --------- -See `proxy-socket-examples` module: `UdpEchoWithProxyProtocol`, `TcpEchoWithProxyProtocol`. - +## Examples +See `proxy-socket-examples` module: `UdpEchoWithProxyProtocol`, `TcpEchoWithProxyProtocol`. From 5b8a9bb7b1a66542ca1a797ff21e4528b71dd691 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Fri, 7 Nov 2025 10:41:59 +0100 Subject: [PATCH 09/31] Split decoder in functions --- .../core/v2/ProxyProtocolV2Decoder.java | 183 +++++++++++------- 1 file changed, 114 insertions(+), 69 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index 485a8a6..af6b6f0 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -50,17 +50,8 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox int fam = famProto & 0xF0; int proto = famProto & 0x0F; - ProxyHeader.AddressFamily af = switch (fam) { - case 0x10 -> ProxyHeader.AddressFamily.INET4; - case 0x20 -> ProxyHeader.AddressFamily.INET6; - case 0x30 -> ProxyHeader.AddressFamily.UNIX; - default -> ProxyHeader.AddressFamily.UNSPEC; - }; - ProxyHeader.TransportProtocol tp = switch (proto) { - case 0x01 -> ProxyHeader.TransportProtocol.STREAM; // TCP - case 0x02 -> ProxyHeader.TransportProtocol.DGRAM; // UDP - default -> ProxyHeader.TransportProtocol.UNSPEC; - }; + ProxyHeader.AddressFamily af = parseAddressFamily(fam); + ProxyHeader.TransportProtocol tp = parseTransportProtocol(proto); int variableLength = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); @@ -68,68 +59,121 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox if (headerLen > length) throw new ProxyProtocolParseException("Insufficient data for header"); int addrStart = pos; - InetSocketAddress src = null; - InetSocketAddress dst = null; + AddressPair addresses = null; if (command == ProxyHeader.Command.PROXY) { - if (af == ProxyHeader.AddressFamily.INET4 && tp != ProxyHeader.TransportProtocol.UNSPEC) { - if (variableLength < 2*(IPV4_ADDR_LEN + PORT_LEN)) { - throw new ProxyProtocolParseException("Truncated IPv4 address block in header"); - } - - // Extract source and destination addresses - InetAddress s; - InetAddress d; - try { - s = InetAddress.getByAddress(new byte[]{data[pos++], data[pos++], data[pos++], data[pos++]}); - d = InetAddress.getByAddress(new byte[]{data[pos++], data[pos++], data[pos++], data[pos++]}); - } catch (UnknownHostException e) { - throw new ProxyProtocolParseException("Invalid IPv4 address in header", e); - } - // Extract source and destination ports - int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - src = new InetSocketAddress(s, sp); - dst = new InetSocketAddress(d, dp); - - } else if (af == ProxyHeader.AddressFamily.INET6 && tp != ProxyHeader.TransportProtocol.UNSPEC) { - if (variableLength < 2*(IPV6_ADDR_LEN + PORT_LEN)) { - throw new ProxyProtocolParseException("Truncated IPv6 address block in header"); - } - - // Extract source and destination addresses - InetAddress s; - InetAddress d; - byte[] sb = new byte[16]; - byte[] db = new byte[16]; - System.arraycopy(data, pos, sb, 0, IPV6_ADDR_LEN); - System.arraycopy(data, pos+IPV6_ADDR_LEN, db, 0, IPV6_ADDR_LEN); - try { - s = InetAddress.getByAddress(sb); - d = InetAddress.getByAddress(db); - } catch (UnknownHostException e) { - throw new ProxyProtocolParseException("Invalid IPv6 address in header", e); - } - - pos += 2*IPV6_ADDR_LEN; - // Extract source and destination ports - int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - src = new InetSocketAddress(s, sp); - dst = new InetSocketAddress(d, dp); - - } else if (af == ProxyHeader.AddressFamily.UNIX) { - // 108 + 108 bytes path - if (variableLength < 2*UNIX_ADDR_LEN) { - throw new ProxyProtocolParseException("Truncated UNIX address block in header"); - } - pos += 2*UNIX_ADDR_LEN; - throw new ProxyProtocolParseException("UNIX Address Processing not implemented"); - } + addresses = parseAddresses(data, pos, af, tp, variableLength); + pos = addresses.newPos; } int consumed = pos - addrStart; int tlvLen = Math.max(0, variableLength - consumed); + List tlvs = parseTlvs(data, pos, tlvLen); + + InetSocketAddress src = addresses != null ? addresses.src : null; + InetSocketAddress dst = addresses != null ? addresses.dst : null; + + return new ProxyHeader(command, af, tp, src, dst, tlvs, headerLen); + } + + private static ProxyHeader.AddressFamily parseAddressFamily(int fam) { + return switch (fam) { + case 0x10 -> ProxyHeader.AddressFamily.INET4; + case 0x20 -> ProxyHeader.AddressFamily.INET6; + case 0x30 -> ProxyHeader.AddressFamily.UNIX; + default -> ProxyHeader.AddressFamily.UNSPEC; + }; + } + + private static ProxyHeader.TransportProtocol parseTransportProtocol(int proto) { + return switch (proto) { + case 0x01 -> ProxyHeader.TransportProtocol.STREAM; + case 0x02 -> ProxyHeader.TransportProtocol.DGRAM; + default -> ProxyHeader.TransportProtocol.UNSPEC; + }; + } + + private static class AddressPair { + final InetSocketAddress src; + final InetSocketAddress dst; + final int newPos; + + AddressPair(InetSocketAddress src, InetSocketAddress dst, int newPos) { + this.src = src; + this.dst = dst; + this.newPos = newPos; + } + } + + private static AddressPair parseAddresses(byte[] data, int pos, ProxyHeader.AddressFamily af, ProxyHeader.TransportProtocol tp, int variableLength) + throws ProxyProtocolParseException { + if (af == ProxyHeader.AddressFamily.INET4 && tp != ProxyHeader.TransportProtocol.UNSPEC) { + return parseIPv4Addresses(data, pos, variableLength); + } else if (af == ProxyHeader.AddressFamily.INET6 && tp != ProxyHeader.TransportProtocol.UNSPEC) { + return parseIPv6Addresses(data, pos, variableLength); + } else if (af == ProxyHeader.AddressFamily.UNIX) { + return parseUnixAddresses(data, pos, variableLength); + } + return null; + } + + private static AddressPair parseIPv4Addresses(byte[] data, int pos, int variableLength) + throws ProxyProtocolParseException { + if (variableLength < 2*(IPV4_ADDR_LEN + PORT_LEN)) { + throw new ProxyProtocolParseException("Truncated IPv4 address block in header"); + } + + InetAddress s; + InetAddress d; + try { + s = InetAddress.getByAddress(new byte[]{data[pos++], data[pos++], data[pos++], data[pos++]}); + d = InetAddress.getByAddress(new byte[]{data[pos++], data[pos++], data[pos++], data[pos++]}); + } catch (UnknownHostException e) { + throw new ProxyProtocolParseException("Invalid IPv4 address in header", e); + } + + int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + + return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), pos); + } + + private static AddressPair parseIPv6Addresses(byte[] data, int pos, int variableLength) + throws ProxyProtocolParseException { + if (variableLength < 2*(IPV6_ADDR_LEN + PORT_LEN)) { + throw new ProxyProtocolParseException("Truncated IPv6 address block in header"); + } + + InetAddress s; + InetAddress d; + byte[] sb = new byte[16]; + byte[] db = new byte[16]; + System.arraycopy(data, pos, sb, 0, IPV6_ADDR_LEN); + System.arraycopy(data, pos+IPV6_ADDR_LEN, db, 0, IPV6_ADDR_LEN); + try { + s = InetAddress.getByAddress(sb); + d = InetAddress.getByAddress(db); + } catch (UnknownHostException e) { + throw new ProxyProtocolParseException("Invalid IPv6 address in header", e); + } + + pos += 2*IPV6_ADDR_LEN; + int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + + return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), pos); + } + + private static AddressPair parseUnixAddresses(byte[] data, int pos, int variableLength) + throws ProxyProtocolParseException { + if (variableLength < 2*UNIX_ADDR_LEN) { + throw new ProxyProtocolParseException("Truncated UNIX address block in header"); + } + pos += 2*UNIX_ADDR_LEN; + throw new ProxyProtocolParseException("UNIX Address Processing not implemented"); + } + + private static List parseTlvs(byte[] data, int pos, int tlvLen) { List tlvs = new ArrayList<>(); int tlvPos = pos; int tlvEnd = tlvPos + tlvLen; @@ -141,9 +185,10 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox tlvs.add(new Tlv(type, data, tlvPos, len)); tlvPos += len; } - - return new ProxyHeader(command, af, tp, src, dst, tlvs, headerLen); + return tlvs; } + + } From 9d7cd72ef6747d1fae8d4a410a925446224f483b Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Fri, 7 Nov 2025 11:23:05 +0100 Subject: [PATCH 10/31] Switch to external library for encoding headers in tests --- proxy-socket-core/pom.xml | 7 ++ .../core/v2/ProxyProtocolV2Decoder.java | 4 +- .../core/v2/ProxyProtocolV2Encoder.java | 119 ------------------ .../core/v2/AwsProxyEncoderHelper.java | 103 +++++++++++++++ .../core/v2/ProxyProtocolV2DecoderTest.java | 66 +++++++++- .../core/v2/ProxyProtocolV2Test.java | 71 ++++------- 6 files changed, 201 insertions(+), 169 deletions(-) delete mode 100644 proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java create mode 100644 proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java diff --git a/proxy-socket-core/pom.xml b/proxy-socket-core/pom.xml index de118de..c4c601f 100644 --- a/proxy-socket-core/pom.xml +++ b/proxy-socket-core/pom.xml @@ -24,6 +24,13 @@ ${junit.version} test + + + com.amazonaws.proprot + proprot + 1.0 + test + diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index af6b6f0..a4e7b7b 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -63,7 +63,9 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox if (command == ProxyHeader.Command.PROXY) { addresses = parseAddresses(data, pos, af, tp, variableLength); - pos = addresses.newPos; + if (addresses != null) { + pos = addresses.newPos; + } } int consumed = pos - addrStart; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java deleted file mode 100644 index 2070b2c..0000000 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Encoder.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * MIT License - * Copyright (c) 2025 Semtech - */ -package net.airvantage.proxysocket.core.v2; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -/** - * Minimal builder for generating Proxy Protocol v2 headers for tests. - */ -public final class ProxyProtocolV2Encoder { - private static final byte[] SIG = "\r\n\r\n\0\r\nQUIT\n".getBytes(StandardCharsets.ISO_8859_1); - - private ProxyHeader.Command command = ProxyHeader.Command.PROXY; - private ProxyHeader.AddressFamily family = ProxyHeader.AddressFamily.INET4; - private ProxyHeader.TransportProtocol socket = ProxyHeader.TransportProtocol.STREAM; - private InetSocketAddress source; - private InetSocketAddress destination; - private final List tlvs = new ArrayList<>(); - - public ProxyProtocolV2Encoder command(ProxyHeader.Command c) { this.command = c; return this; } - public ProxyProtocolV2Encoder family(ProxyHeader.AddressFamily f) { this.family = f; return this; } - public ProxyProtocolV2Encoder socket(ProxyHeader.TransportProtocol p) { this.socket = p; return this; } - public ProxyProtocolV2Encoder source(InetSocketAddress s) { this.source = s; return this; } - public ProxyProtocolV2Encoder destination(InetSocketAddress d) { this.destination = d; return this; } - public ProxyProtocolV2Encoder addTlv(int type, byte[] value) { this.tlvs.add(new Tlv(type, value, 0, value.length)); return this; } - - public byte[] build() { - byte verCmd = (byte) ((2 << 4) | (command == ProxyHeader.Command.PROXY ? 0x01 : 0x00)); - int fam = switch (family) { case INET4 -> 0x10; case INET6 -> 0x20; case UNIX -> 0x30; default -> 0x00; }; - int proto = switch (socket) { case STREAM -> 0x01; case DGRAM -> 0x02; default -> 0x00; }; - byte famProto = (byte) (fam | proto); - - byte[] addr = buildAddr(); - byte[] tlvBytes = buildTlvs(); - int addrLen = addr.length + tlvBytes.length; - byte[] out = new byte[16 + addrLen]; - int p = 0; - System.arraycopy(SIG, 0, out, p, SIG.length); p += SIG.length; - out[p++] = verCmd; - out[p++] = famProto; - out[p++] = (byte) ((addrLen >>> 8) & 0xFF); - out[p++] = (byte) (addrLen & 0xFF); - System.arraycopy(addr, 0, out, p, addr.length); p += addr.length; - System.arraycopy(tlvBytes, 0, out, p, tlvBytes.length); - return out; - } - - private byte[] buildAddr() { - if (command == ProxyHeader.Command.LOCAL) return new byte[0]; - if (family == ProxyHeader.AddressFamily.INET4 && (socket == ProxyHeader.TransportProtocol.STREAM || socket == ProxyHeader.TransportProtocol.DGRAM)) { - byte[] b = new byte[12]; - writeIPv4PortPair(b); - return b; - } - if (family == ProxyHeader.AddressFamily.INET6 && (socket == ProxyHeader.TransportProtocol.STREAM || socket == ProxyHeader.TransportProtocol.DGRAM)) { - byte[] b = new byte[36]; - writeIPv6PortPair(b); - return b; - } - return new byte[0]; - } - - private void writeIPv4PortPair(byte[] b) { - byte[] src = source == null ? new byte[4] : source.getAddress().getAddress(); - byte[] dst = destination == null ? new byte[4] : destination.getAddress().getAddress(); - System.arraycopy(src, 0, b, 0, 4); - System.arraycopy(dst, 0, b, 4, 4); - int sp = source == null ? 0 : source.getPort(); - int dp = destination == null ? 0 : destination.getPort(); - b[8] = (byte) ((sp >>> 8) & 0xFF); b[9] = (byte) (sp & 0xFF); - b[10] = (byte) ((dp >>> 8) & 0xFF); b[11] = (byte) (dp & 0xFF); - } - - private void writeIPv6PortPair(byte[] b) { - byte[] src = source == null ? new byte[16] : toIPv6Bytes(source.getAddress()); - byte[] dst = destination == null ? new byte[16] : toIPv6Bytes(destination.getAddress()); - System.arraycopy(src, 0, b, 0, 16); - System.arraycopy(dst, 0, b, 16, 16); - int sp = source == null ? 0 : source.getPort(); - int dp = destination == null ? 0 : destination.getPort(); - b[32] = (byte) ((sp >>> 8) & 0xFF); b[33] = (byte) (sp & 0xFF); - b[34] = (byte) ((dp >>> 8) & 0xFF); b[35] = (byte) (dp & 0xFF); - } - - private static byte[] toIPv6Bytes(InetAddress addr) { - byte[] a = addr.getAddress(); - if (a.length == 16) return a; - // IPv4-mapped IPv6 ::ffff:a.b.c.d - byte[] v6 = new byte[16]; - v6[10] = (byte) 0xFF; v6[11] = (byte) 0xFF; - System.arraycopy(a, 0, v6, 12, 4); - return v6; - } - - private byte[] buildTlvs() { - int total = 0; - for (Tlv t : tlvs) total += 3 + (t.getValue() == null ? 0 : t.getValue().length); - byte[] buf = new byte[total]; - int p = 0; - for (Tlv t : tlvs) { - byte[] v = t.getValue(); - int len = v == null ? 0 : v.length; - buf[p++] = (byte) (t.getType() & 0xFF); - buf[p++] = (byte) ((len >>> 8) & 0xFF); - buf[p++] = (byte) (len & 0xFF); - if (len > 0) { - System.arraycopy(v, 0, buf, p, len); - p += len; - } - } - return buf; - } -} diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java new file mode 100644 index 0000000..b351005 --- /dev/null +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java @@ -0,0 +1,103 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + * + * Helper class to encode PROXY protocol v2 headers using AWS ProProt library. + */ +package net.airvantage.proxysocket.core.v2; + +import com.amazonaws.proprot.Header; +import com.amazonaws.proprot.ProxyProtocol; +import com.amazonaws.proprot.ProxyProtocolSpec; +import com.amazonaws.proprot.TlvRaw; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; + +/** + * Helper class to encode PROXY protocol v2 headers using AWS ProProt library. + * This is used in tests to validate our decoder against a known working encoder. + */ +public final class AwsProxyEncoderHelper { + private ProxyProtocolSpec.Command command = ProxyProtocolSpec.Command.PROXY; + private ProxyProtocolSpec.AddressFamily family = ProxyProtocolSpec.AddressFamily.AF_INET; + private ProxyProtocolSpec.TransportProtocol protocol = ProxyProtocolSpec.TransportProtocol.STREAM; + private InetSocketAddress source; + private InetSocketAddress destination; + private final Header header = new Header(); + + public AwsProxyEncoderHelper command(ProxyHeader.Command cmd) { + this.command = cmd == ProxyHeader.Command.LOCAL + ? ProxyProtocolSpec.Command.LOCAL + : ProxyProtocolSpec.Command.PROXY; + return this; + } + + public AwsProxyEncoderHelper family(ProxyHeader.AddressFamily fam) { + this.family = switch (fam) { + case UNSPEC -> ProxyProtocolSpec.AddressFamily.AF_UNSPEC; + case INET4 -> ProxyProtocolSpec.AddressFamily.AF_INET; + case INET6 -> ProxyProtocolSpec.AddressFamily.AF_INET6; + case UNIX -> ProxyProtocolSpec.AddressFamily.AF_UNIX; + }; + return this; + } + + public AwsProxyEncoderHelper socket(ProxyHeader.TransportProtocol proto) { + this.protocol = switch (proto) { + case UNSPEC -> ProxyProtocolSpec.TransportProtocol.UNSPEC; + case STREAM -> ProxyProtocolSpec.TransportProtocol.STREAM; + case DGRAM -> ProxyProtocolSpec.TransportProtocol.DGRAM; + }; + return this; + } + + public AwsProxyEncoderHelper source(InetSocketAddress src) { + this.source = src; + return this; + } + + public AwsProxyEncoderHelper destination(InetSocketAddress dst) { + this.destination = dst; + return this; + } + + public AwsProxyEncoderHelper addTlv(int type, byte[] value) { + TlvRaw tlv = new TlvRaw(); + tlv.setType(type); + tlv.setValue(value); + header.addTlv(tlv); + return this; + } + + public byte[] build() throws IOException { + header.setCommand(command); + header.setAddressFamily(family); + header.setTransportProtocol(protocol); + + // AWS ProProt validates addresses even for LOCAL command, set dummy values + if (command == ProxyProtocolSpec.Command.LOCAL && source == null) { + header.setSrcAddress(new byte[]{0, 0, 0, 0}); + header.setDstAddress(new byte[]{0, 0, 0, 0}); + header.setSrcPort(0); + header.setDstPort(0); + } else { + if (source != null) { + header.setSrcAddress(source.getAddress().getAddress()); + header.setSrcPort(source.getPort()); + } + + if (destination != null) { + header.setDstAddress(destination.getAddress().getAddress()); + header.setDstPort(destination.getPort()); + } + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProxyProtocol proxyProtocol = new ProxyProtocol(); + proxyProtocol.write(header, out); + return out.toByteArray(); + } +} + diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java index 0a25710..ae634fc 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java @@ -1,5 +1,10 @@ -package net.airvantage.proxysocket.core.v2; +/* + * MIT License + * Copyright (c) 2025 Semtech + * Validation of ProxyProtocolV2Decoder against hardcoded headers for known cases + */ +package net.airvantage.proxysocket.core.v2; import net.airvantage.proxysocket.core.ProxyProtocolParseException; import org.junit.jupiter.api.Test; @@ -134,6 +139,65 @@ void invalidVersion() throws Exception { assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length)); } + @Test + void localHeaderHasLength16() throws Exception { + // Hand-crafted LOCAL header with exactly 16 bytes + byte verCmd = (byte) 0x20; // v2, LOCAL + byte famProto = (byte) 0x00; // UNSPEC + UNSPEC + byte[] h = new byte[SIG.length + 4]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; + h[p++] = verCmd; h[p++] = famProto; + h[p++] = 0x00; h[p++] = 0x00; // length = 0 + + assertEquals(16, h.length); + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + assertEquals(16, parsed.getHeaderLength()); + } + + @Test + void addressLengthBeyondBufferInvalid() { + // Hand-crafted header with declared length exceeding actual data + byte verCmd = (byte) 0x21; // v2, PROXY + byte famProto = (byte) 0x11; // INET4 + STREAM + byte[] h = new byte[SIG.length + 4]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; + h[p++] = verCmd; h[p++] = famProto; + h[p++] = 0x00; h[p++] = 0x01; // length = 1 (but no data follows) + + assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length)); + } + + @Test + void tlvLengthOverrunIgnored() throws Exception { + // Hand-crafted header with TLV whose length exceeds available buffer + byte verCmd = (byte) 0x21; // v2, PROXY + byte famProto = (byte) 0x11; // INET4 + STREAM + + // IPv4 addresses + ports (12 bytes) + TLV (4 bytes) + byte[] payload = new byte[]{ + // src 127.0.0.1 + 0x7F, 0x00, 0x00, 0x01, + // dst 127.0.0.2 + 0x7F, 0x00, 0x00, 0x02, + // sport 1234, dport 5678 + 0x04, (byte) 0xD2, 0x16, 0x2E, + // TLV: type=0x01, length=0x7FFF (huge, intentionally invalid) + 0x01, 0x7F, (byte) 0xFF, 0x42 + }; + + byte[] h = new byte[SIG.length + 4 + payload.length]; + int p = 0; System.arraycopy(SIG, 0, h, p, SIG.length); p += SIG.length; + h[p++] = verCmd; h[p++] = famProto; + h[p++] = (byte) ((payload.length >>> 8) & 0xFF); + h[p++] = (byte) (payload.length & 0xFF); + System.arraycopy(payload, 0, h, p, payload.length); + + // Parser should handle gracefully without throwing + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + assertNotNull(parsed); + assertEquals(ProxyHeader.AddressFamily.INET4, parsed.getFamily()); + } + } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java index 4d820fa..88aeaf2 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -1,8 +1,15 @@ +/* + * MIT License + * Copyright (c) 2025 Semtech + + * Validation of ProxyProtocolV2Decoder using AWS ProProt library + */ package net.airvantage.proxysocket.core.v2; import net.airvantage.proxysocket.core.ProxyProtocolParseException; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; @@ -11,7 +18,7 @@ class ProxyProtocolV2Test { @Test void parseIPv4Tcp() throws Exception { - var header = new ProxyProtocolV2Encoder() + var header = new AwsProxyEncoderHelper() .family(ProxyHeader.AddressFamily.INET4) .socket(ProxyHeader.TransportProtocol.STREAM) .source(new InetSocketAddress("127.0.0.1", 12345)) @@ -28,24 +35,27 @@ void parseIPv4Tcp() throws Exception { @Test void parseIPv6UdpWithTlv() throws Exception { - var header = new ProxyProtocolV2Encoder() + var header = new AwsProxyEncoderHelper() .family(ProxyHeader.AddressFamily.INET6) .socket(ProxyHeader.TransportProtocol.DGRAM) .source(new InetSocketAddress("::1", 1000)) .destination(new InetSocketAddress("::2", 2000)) - .addTlv(0x01, new byte[]{0x41, 0x42}) + .addTlv(0xEA, new byte[]{0x41, 0x42}) // Use non-reserved TLV type .build(); ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertEquals(ProxyHeader.AddressFamily.INET6, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); - assertEquals(1, parsed.getTlvs().size()); - assertArrayEquals(new byte[]{0x41, 0x42}, parsed.getTlvs().get(0).getValue()); + // AWS ProProt adds extra TLVs, find our custom TLV + assertTrue(parsed.getTlvs().size() >= 1); + boolean foundCustomTlv = parsed.getTlvs().stream() + .anyMatch(tlv -> tlv.getType() == 0xEA && java.util.Arrays.equals(tlv.getValue(), new byte[]{0x41, 0x42})); + assertTrue(foundCustomTlv, "Should find custom TLV with type 0xEA"); } @Test void parseLocal() throws Exception { - var header = new ProxyProtocolV2Encoder() + var header = new AwsProxyEncoderHelper() .command(ProxyHeader.Command.LOCAL) .build(); @@ -54,24 +64,9 @@ void parseLocal() throws Exception { assertNull(parsed.getSourceAddress()); } - - @Test - void tlvLengthOverrunIgnored() throws Exception { - // Build header then break TLV length to exceed buffer; parser should stop TLV loop gracefully - var builder = new ProxyProtocolV2Encoder() - .addTlv(0x01, new byte[]{0x01}); - byte[] h = builder.build(); - // Set TLV length to something large - int tlvStart = h.length - 3 - 1; // type(1) + len(2) + value(1) - h[tlvStart + 1] = (byte) 0x7F; - h[tlvStart + 2] = (byte) 0x7F; - ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); - assertNotNull(parsed); - } - @Test void parseIPv4Udp() throws Exception { - var header = new ProxyProtocolV2Encoder() + var header = new AwsProxyEncoderHelper() .family(ProxyHeader.AddressFamily.INET4) .socket(ProxyHeader.TransportProtocol.DGRAM) .source(new InetSocketAddress("127.0.0.1", 1111)) @@ -85,33 +80,10 @@ void parseIPv4Udp() throws Exception { assertEquals(2222, parsed.getDestinationAddress().getPort()); } - @Test - void localHeaderHasLength16() throws Exception { - var header = new ProxyProtocolV2Encoder() - .command(ProxyHeader.Command.LOCAL) - .build(); - assertEquals(16, header.length); - ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); - assertEquals(16, parsed.getHeaderLength()); - } - - - - @Test - void addressLengthBeyondBufferInvalid() { - byte[] h = new ProxyProtocolV2Encoder().build(); - // Bump declared length by one without providing data - int lenPos = 14; - int len = ((h[lenPos] & 0xFF) << 8) | (h[lenPos + 1] & 0xFF); - len += 1; - h[lenPos] = (byte) ((len >>> 8) & 0xFF); - h[lenPos + 1] = (byte) (len & 0xFF); - assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length)); - } @Test void proxyUnspecWithTlvOnly() throws Exception { - var header = new ProxyProtocolV2Encoder() + var header = new AwsProxyEncoderHelper() .family(ProxyHeader.AddressFamily.UNSPEC) .socket(ProxyHeader.TransportProtocol.UNSPEC) .addTlv(0xEE, new byte[]{0x10, 0x20, 0x30}) @@ -119,8 +91,11 @@ void proxyUnspecWithTlvOnly() throws Exception { ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertEquals(ProxyHeader.AddressFamily.UNSPEC, parsed.getFamily()); - assertEquals(1, parsed.getTlvs().size()); - assertArrayEquals(new byte[]{0x10, 0x20, 0x30}, parsed.getTlvs().get(0).getValue()); + // AWS ProProt adds extra TLVs, find our custom TLV + assertTrue(parsed.getTlvs().size() >= 1); + boolean foundCustomTlv = parsed.getTlvs().stream() + .anyMatch(tlv -> tlv.getType() == 0xEE && java.util.Arrays.equals(tlv.getValue(), new byte[]{0x10, 0x20, 0x30})); + assertTrue(foundCustomTlv, "Should find custom TLV with type 0xEE"); } } From 50e65a2c96cbafa0cc803d2a508eba108b5ed6e2 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Fri, 7 Nov 2025 11:24:26 +0100 Subject: [PATCH 11/31] whitespaces --- .../proxysocket/core/v2/ProxyProtocolV2Decoder.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index a4e7b7b..9ad35ca 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -189,9 +189,4 @@ private static List parseTlvs(byte[] data, int pos, int tlvLen) { } return tlvs; } - - } - - - From f4292cfef2ef85c0ee2f52dd0e4500b744fa1dbd Mon Sep 17 00:00:00 2001 From: bplessis14821 Date: Mon, 17 Nov 2025 15:03:15 +0100 Subject: [PATCH 12/31] Update README.md Co-authored-by: Simon Bernard --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b41acc2..08435b4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Library providing HAProxy Proxy Protocol v2 support for UDP and TCP. Multi-modul - proxy-socket-guava: optional Guava-based cache - proxy-socket-examples: runnable samples -Reference: [HAProxy Proxy Protocol Specifications](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) +Reference: [HAProxy Proxy Protocol Specifications](https://www.haproxy.org/download/3.3/doc/proxy-protocol.txt) ## Quick start (UDP) From c47fb02faf05f13f6619e8381d3167fc6e7c8b77 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Mon, 17 Nov 2025 15:10:04 +0100 Subject: [PATCH 13/31] Fix license file, use leshan compatible BSD-3-Clause --- LICENSE | 684 ------------------------------------------- LICENSE.BSD-3-Clause | 27 ++ 2 files changed, 27 insertions(+), 684 deletions(-) delete mode 100644 LICENSE create mode 100644 LICENSE.BSD-3-Clause diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 0f59d12..0000000 --- a/LICENSE +++ /dev/null @@ -1,684 +0,0 @@ -MIT License - -Copyright (c) 2025 Semtech - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -sockets for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/LICENSE.BSD-3-Clause b/LICENSE.BSD-3-Clause new file mode 100644 index 0000000..0a802bc --- /dev/null +++ b/LICENSE.BSD-3-Clause @@ -0,0 +1,27 @@ +Copyright (c) 2025, Semtech Corporation. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 47e6df3f14d61214aacab6d71d4c9a8bee3a612e Mon Sep 17 00:00:00 2001 From: bplessis14821 Date: Mon, 17 Nov 2025 15:12:45 +0100 Subject: [PATCH 14/31] Update proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java Co-authored-by: Simon Bernard --- .../java/net/airvantage/proxysocket/core/v2/ProxyHeader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java index a97522f..386051d 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java @@ -11,7 +11,7 @@ public final class ProxyHeader { public enum Command { LOCAL, PROXY } public enum AddressFamily { INET4, INET6, UNIX, UNSPEC } - public enum TransportProtocol { STREAM, DGRAM, UNSPEC } + public enum TransportProtocol { UNSPEC, STREAM, DGRAM } private final Command command; private final AddressFamily family; From 9029e73a5812577a9b47652b19f4bbf8809067de Mon Sep 17 00:00:00 2001 From: bplessis14821 Date: Mon, 17 Nov 2025 15:13:44 +0100 Subject: [PATCH 15/31] Update proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java Co-authored-by: Simon Bernard --- .../java/net/airvantage/proxysocket/core/v2/ProxyHeader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java index 386051d..dd8c086 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java @@ -10,7 +10,7 @@ public final class ProxyHeader { public enum Command { LOCAL, PROXY } - public enum AddressFamily { INET4, INET6, UNIX, UNSPEC } + public enum AddressFamily { AF_UNSPEC, AF_INET, AF_INET6, AF_UNIX } public enum TransportProtocol { UNSPEC, STREAM, DGRAM } private final Command command; From 340dde3b94d373bdb24327207a5107cb41dff36d Mon Sep 17 00:00:00 2001 From: bplessis14821 Date: Mon, 17 Nov 2025 15:15:23 +0100 Subject: [PATCH 16/31] Update proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java Co-authored-by: Simon Bernard --- .../airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index 9ad35ca..b54e6df 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -18,7 +18,7 @@ public final class ProxyProtocolV2Decoder { private ProxyProtocolV2Decoder() {} - private static final byte[] SIG = "\r\n\r\n\0\r\nQUIT\n".getBytes(StandardCharsets.ISO_8859_1); + private static final byte[] PROTOCOL_SIGNATURE = new byte[] {0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A}; private static final int IPV4_ADDR_LEN = 4; private static final int IPV6_ADDR_LEN = 16; private static final int UNIX_ADDR_LEN = 216; From eda9898731694976445395df1cebe0ca0a0199a4 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 11:44:57 +0100 Subject: [PATCH 17/31] take in account simon's review --- .../core/v2/ProxyProtocolV2Decoder.java | 112 ++++++++++-------- .../core/v2/AwsProxyEncoderHelper.java | 8 +- .../core/v2/ProxyProtocolV2DecoderTest.java | 8 +- .../core/v2/ProxyProtocolV2Test.java | 14 +-- 4 files changed, 80 insertions(+), 62 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index b54e6df..4894791 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -19,33 +19,46 @@ public final class ProxyProtocolV2Decoder { private ProxyProtocolV2Decoder() {} private static final byte[] PROTOCOL_SIGNATURE = new byte[] {0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A}; + private static final int PROTOCOL_SIGNATURE_FIXED_LENGTH = PROTOCOL_SIGNATURE.length + 4; private static final int IPV4_ADDR_LEN = 4; private static final int IPV6_ADDR_LEN = 16; private static final int UNIX_ADDR_LEN = 216; private static final int PORT_LEN = 2; private static final int TLV_HEADER_LEN = 3; - public static ProxyHeader parse(byte[] data, int offset, int length) throws ProxyProtocolParseException { - if (data == null || offset < 0 || length < 0) throw new ProxyProtocolParseException("Null data"); - if ((length+offset) > data.length) throw new ProxyProtocolParseException("Invalid offset/length"); + public static ProxyHeader parse(byte[] data, int offset, int length) throws ProxyProtocolParseException, IllegalArgumentException { + if (data == null || offset < 0 || length < 0) throw new IllegalArgumentException("Invalid arguments"); + if ((length+offset) > data.length) throw new IllegalArgumentException("Invalid offset/length combination with data length"); int end = offset + length; - if (SIG.length + 4 > length) throw new ProxyProtocolParseException("Insufficient data for header"); + if (PROTOCOL_SIGNATURE_FIXED_LENGTH > length) throw new ProxyProtocolParseException("Insufficient data for header"); - for (int i = 0; i < SIG.length; i++) { - if (data[offset + i] != SIG[i]) { + for (int i = 0; i < PROTOCOL_SIGNATURE.length; i++) { + if (data[offset + i] != PROTOCOL_SIGNATURE[i]) { throw new ProxyProtocolParseException("Invalid signature"); } } - int pos = offset + SIG.length; + int pos = offset + PROTOCOL_SIGNATURE.length; - int verCmd = data[pos++] & 0xFF; // version/command + // Byte 13: version/command + int verCmd = data[pos++] & 0xFF; int version = (verCmd >> 4) & 0x0F; if (version != 2) throw new ProxyProtocolParseException("Invalid version"); int cmd = verCmd & 0x0F; - ProxyHeader.Command command = cmd == 0x00 ? ProxyHeader.Command.LOCAL : ProxyHeader.Command.PROXY; + ProxyHeader.Command command; + switch (cmd) { + case 0x00: + // Early return for LOCAL command + return new ProxyHeader(ProxyHeader.Command.LOCAL, ProxyHeader.AddressFamily.AF_UNSPEC, ProxyHeader.TransportProtocol.UNSPEC, null, null, null, PROTOCOL_SIGNATURE_FIXED_LENGTH); + case 0x01: + command = ProxyHeader.Command.PROXY; + break; + default: + throw new ProxyProtocolParseException("Invalid command"); + } + // Byte 14: address family and protocol int famProto = data[pos++] & 0xFF; int fam = famProto & 0xF0; int proto = famProto & 0x0F; @@ -53,23 +66,20 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox ProxyHeader.AddressFamily af = parseAddressFamily(fam); ProxyHeader.TransportProtocol tp = parseTransportProtocol(proto); + // Byte 15, 16: Length of address part of the header, including TLVs int variableLength = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - int headerLen = SIG.length + 4 + variableLength; + // Check if we have enough data for the header + int headerLen = PROTOCOL_SIGNATURE_FIXED_LENGTH + variableLength; if (headerLen > length) throw new ProxyProtocolParseException("Insufficient data for header"); - int addrStart = pos; AddressPair addresses = null; - - if (command == ProxyHeader.Command.PROXY) { - addresses = parseAddresses(data, pos, af, tp, variableLength); - if (addresses != null) { - pos = addresses.newPos; - } + if (af != ProxyHeader.AddressFamily.AF_UNSPEC) { + addresses = parseAddresses(data, pos, af, variableLength); + pos += addresses.bytesConsumed; } - int consumed = pos - addrStart; - int tlvLen = Math.max(0, variableLength - consumed); + int tlvLen = Math.max(0, variableLength - (addresses != null ? addresses.bytesConsumed : 0)); List tlvs = parseTlvs(data, pos, tlvLen); InetSocketAddress src = addresses != null ? addresses.src : null; @@ -78,50 +88,53 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox return new ProxyHeader(command, af, tp, src, dst, tlvs, headerLen); } - private static ProxyHeader.AddressFamily parseAddressFamily(int fam) { + private static ProxyHeader.AddressFamily parseAddressFamily(int fam) + throws ProxyProtocolParseException { return switch (fam) { - case 0x10 -> ProxyHeader.AddressFamily.INET4; - case 0x20 -> ProxyHeader.AddressFamily.INET6; - case 0x30 -> ProxyHeader.AddressFamily.UNIX; - default -> ProxyHeader.AddressFamily.UNSPEC; + case 0x00 -> ProxyHeader.AddressFamily.AF_UNSPEC; + case 0x10 -> ProxyHeader.AddressFamily.AF_INET; + case 0x20 -> ProxyHeader.AddressFamily.AF_INET6; + case 0x30 -> ProxyHeader.AddressFamily.AF_UNIX; + default -> throw new ProxyProtocolParseException("Invalid address family"); }; } - private static ProxyHeader.TransportProtocol parseTransportProtocol(int proto) { + private static ProxyHeader.TransportProtocol parseTransportProtocol(int proto) + throws ProxyProtocolParseException { return switch (proto) { + case 0x00 -> ProxyHeader.TransportProtocol.UNSPEC; case 0x01 -> ProxyHeader.TransportProtocol.STREAM; case 0x02 -> ProxyHeader.TransportProtocol.DGRAM; - default -> ProxyHeader.TransportProtocol.UNSPEC; + default -> throw new ProxyProtocolParseException("Invalid transport protocol"); }; } private static class AddressPair { final InetSocketAddress src; final InetSocketAddress dst; - final int newPos; + final int bytesConsumed; - AddressPair(InetSocketAddress src, InetSocketAddress dst, int newPos) { + AddressPair(InetSocketAddress src, InetSocketAddress dst, int bytesConsumed) { this.src = src; this.dst = dst; - this.newPos = newPos; + this.bytesConsumed = bytesConsumed; } } - private static AddressPair parseAddresses(byte[] data, int pos, ProxyHeader.AddressFamily af, ProxyHeader.TransportProtocol tp, int variableLength) + private static AddressPair parseAddresses(byte[] data, int currentPosition, ProxyHeader.AddressFamily af, int variableLength) throws ProxyProtocolParseException { - if (af == ProxyHeader.AddressFamily.INET4 && tp != ProxyHeader.TransportProtocol.UNSPEC) { - return parseIPv4Addresses(data, pos, variableLength); - } else if (af == ProxyHeader.AddressFamily.INET6 && tp != ProxyHeader.TransportProtocol.UNSPEC) { - return parseIPv6Addresses(data, pos, variableLength); - } else if (af == ProxyHeader.AddressFamily.UNIX) { - return parseUnixAddresses(data, pos, variableLength); - } - return null; + return switch (af) { + case AF_INET -> parseIPv4Addresses(data, currentPosition, variableLength); + case AF_INET6 -> parseIPv6Addresses(data, currentPosition, variableLength); + case AF_UNIX -> parseUnixAddresses(data, currentPosition, variableLength); + default -> throw new ProxyProtocolParseException("Invalid address family"); + }; } + private static int IPV4_ADDR_PAIR_LEN = 2*(IPV4_ADDR_LEN + PORT_LEN); private static AddressPair parseIPv4Addresses(byte[] data, int pos, int variableLength) throws ProxyProtocolParseException { - if (variableLength < 2*(IPV4_ADDR_LEN + PORT_LEN)) { + if (variableLength < IPV4_ADDR_PAIR_LEN) { throw new ProxyProtocolParseException("Truncated IPv4 address block in header"); } @@ -137,19 +150,20 @@ private static AddressPair parseIPv4Addresses(byte[] data, int pos, int variable int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), pos); + return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), IPV4_ADDR_PAIR_LEN); } + private static int IPV6_ADDR_PAIR_LEN = 2*(IPV6_ADDR_LEN + PORT_LEN); private static AddressPair parseIPv6Addresses(byte[] data, int pos, int variableLength) throws ProxyProtocolParseException { - if (variableLength < 2*(IPV6_ADDR_LEN + PORT_LEN)) { + if (variableLength < IPV6_ADDR_PAIR_LEN) { throw new ProxyProtocolParseException("Truncated IPv6 address block in header"); } InetAddress s; InetAddress d; - byte[] sb = new byte[16]; - byte[] db = new byte[16]; + byte[] sb = new byte[IPV6_ADDR_LEN]; + byte[] db = new byte[IPV6_ADDR_LEN]; System.arraycopy(data, pos, sb, 0, IPV6_ADDR_LEN); System.arraycopy(data, pos+IPV6_ADDR_LEN, db, 0, IPV6_ADDR_LEN); try { @@ -163,16 +177,20 @@ private static AddressPair parseIPv6Addresses(byte[] data, int pos, int variable int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), pos); + return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), IPV6_ADDR_PAIR_LEN); } + private static int UNIX_ADDR_PAIR_LEN = 2*UNIX_ADDR_LEN; private static AddressPair parseUnixAddresses(byte[] data, int pos, int variableLength) throws ProxyProtocolParseException { - if (variableLength < 2*UNIX_ADDR_LEN) { + if (variableLength < UNIX_ADDR_PAIR_LEN) { throw new ProxyProtocolParseException("Truncated UNIX address block in header"); } - pos += 2*UNIX_ADDR_LEN; - throw new ProxyProtocolParseException("UNIX Address Processing not implemented"); + + // A receiver is not required to implement other ones, provided that it + // automatically falls back to the UNSPEC mode for the valid combinations above + // that it does not support. + return new AddressPair(null, null, UNIX_ADDR_PAIR_LEN); } private static List parseTlvs(byte[] data, int pos, int tlvLen) { diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java index b351005..885e144 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/AwsProxyEncoderHelper.java @@ -36,10 +36,10 @@ public AwsProxyEncoderHelper command(ProxyHeader.Command cmd) { public AwsProxyEncoderHelper family(ProxyHeader.AddressFamily fam) { this.family = switch (fam) { - case UNSPEC -> ProxyProtocolSpec.AddressFamily.AF_UNSPEC; - case INET4 -> ProxyProtocolSpec.AddressFamily.AF_INET; - case INET6 -> ProxyProtocolSpec.AddressFamily.AF_INET6; - case UNIX -> ProxyProtocolSpec.AddressFamily.AF_UNIX; + case AF_UNSPEC -> ProxyProtocolSpec.AddressFamily.AF_UNSPEC; + case AF_INET -> ProxyProtocolSpec.AddressFamily.AF_INET; + case AF_INET6 -> ProxyProtocolSpec.AddressFamily.AF_INET6; + case AF_UNIX -> ProxyProtocolSpec.AddressFamily.AF_UNIX; }; return this; } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java index ae634fc..28d5f3c 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java @@ -44,7 +44,7 @@ void decodeIPv4Tcp() throws Exception { ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); assertEquals(ProxyHeader.Command.PROXY, parsed.getCommand()); - assertEquals(ProxyHeader.AddressFamily.INET4, parsed.getFamily()); + assertEquals(ProxyHeader.AddressFamily.AF_INET, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.STREAM, parsed.getProtocol()); assertEquals(12345, parsed.getSourceAddress().getPort()); assertEquals(443, parsed.getDestinationAddress().getPort()); @@ -75,7 +75,7 @@ void decodeIPv6Udp() throws Exception { System.arraycopy(addr, 0, h, p, addr.length); ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); - assertEquals(ProxyHeader.AddressFamily.INET6, parsed.getFamily()); + assertEquals(ProxyHeader.AddressFamily.AF_INET6, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); assertEquals(1000, parsed.getSourceAddress().getPort()); assertEquals(2000, parsed.getDestinationAddress().getPort()); @@ -111,7 +111,7 @@ void decodeUnspecWithTlvOnly() throws Exception { System.arraycopy(tlv, 0, h, p, tlv.length); ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); - assertEquals(ProxyHeader.AddressFamily.UNSPEC, parsed.getFamily()); + assertEquals(ProxyHeader.AddressFamily.AF_UNSPEC, parsed.getFamily()); assertNotNull(parsed.getTlvs()); assertEquals(1, parsed.getTlvs().size()); assertArrayEquals(new byte[]{0x10, 0x20, 0x30}, parsed.getTlvs().get(0).getValue()); @@ -195,7 +195,7 @@ void tlvLengthOverrunIgnored() throws Exception { // Parser should handle gracefully without throwing ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); assertNotNull(parsed); - assertEquals(ProxyHeader.AddressFamily.INET4, parsed.getFamily()); + assertEquals(ProxyHeader.AddressFamily.AF_INET, parsed.getFamily()); } } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java index 88aeaf2..bdae470 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -19,7 +19,7 @@ class ProxyProtocolV2Test { @Test void parseIPv4Tcp() throws Exception { var header = new AwsProxyEncoderHelper() - .family(ProxyHeader.AddressFamily.INET4) + .family(ProxyHeader.AddressFamily.AF_INET) .socket(ProxyHeader.TransportProtocol.STREAM) .source(new InetSocketAddress("127.0.0.1", 12345)) .destination(new InetSocketAddress("127.0.0.2", 443)) @@ -27,7 +27,7 @@ void parseIPv4Tcp() throws Exception { ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); assertEquals(ProxyHeader.Command.PROXY, parsed.getCommand()); - assertEquals(ProxyHeader.AddressFamily.INET4, parsed.getFamily()); + assertEquals(ProxyHeader.AddressFamily.AF_INET, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.STREAM, parsed.getProtocol()); assertEquals(12345, parsed.getSourceAddress().getPort()); assertEquals(443, parsed.getDestinationAddress().getPort()); @@ -36,7 +36,7 @@ void parseIPv4Tcp() throws Exception { @Test void parseIPv6UdpWithTlv() throws Exception { var header = new AwsProxyEncoderHelper() - .family(ProxyHeader.AddressFamily.INET6) + .family(ProxyHeader.AddressFamily.AF_INET6) .socket(ProxyHeader.TransportProtocol.DGRAM) .source(new InetSocketAddress("::1", 1000)) .destination(new InetSocketAddress("::2", 2000)) @@ -44,7 +44,7 @@ void parseIPv6UdpWithTlv() throws Exception { .build(); ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); - assertEquals(ProxyHeader.AddressFamily.INET6, parsed.getFamily()); + assertEquals(ProxyHeader.AddressFamily.AF_INET6, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); // AWS ProProt adds extra TLVs, find our custom TLV assertTrue(parsed.getTlvs().size() >= 1); @@ -67,7 +67,7 @@ void parseLocal() throws Exception { @Test void parseIPv4Udp() throws Exception { var header = new AwsProxyEncoderHelper() - .family(ProxyHeader.AddressFamily.INET4) + .family(ProxyHeader.AddressFamily.AF_INET) .socket(ProxyHeader.TransportProtocol.DGRAM) .source(new InetSocketAddress("127.0.0.1", 1111)) .destination(new InetSocketAddress("127.0.0.2", 2222)) @@ -84,13 +84,13 @@ void parseIPv4Udp() throws Exception { @Test void proxyUnspecWithTlvOnly() throws Exception { var header = new AwsProxyEncoderHelper() - .family(ProxyHeader.AddressFamily.UNSPEC) + .family(ProxyHeader.AddressFamily.AF_UNSPEC) .socket(ProxyHeader.TransportProtocol.UNSPEC) .addTlv(0xEE, new byte[]{0x10, 0x20, 0x30}) .build(); ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); - assertEquals(ProxyHeader.AddressFamily.UNSPEC, parsed.getFamily()); + assertEquals(ProxyHeader.AddressFamily.AF_UNSPEC, parsed.getFamily()); // AWS ProProt adds extra TLVs, find our custom TLV assertTrue(parsed.getTlvs().size() >= 1); boolean foundCustomTlv = parsed.getTlvs().stream() From b23bb8f35e0122c8c14291fa94332aff61d70469 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 11:47:18 +0100 Subject: [PATCH 18/31] add serialVersionUID --- .../net/airvantage/proxysocket/core/ProxyProtocolException.java | 2 ++ .../proxysocket/core/ProxyProtocolParseException.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java index 8c1ccf9..17fbd89 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java @@ -5,6 +5,8 @@ package net.airvantage.proxysocket.core; public class ProxyProtocolException extends Exception { + private static final long serialVersionUID = 1L; + public ProxyProtocolException(String message) { super(message); } diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java index d6f3e0e..52775c0 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java @@ -5,6 +5,8 @@ package net.airvantage.proxysocket.core; public final class ProxyProtocolParseException extends ProxyProtocolException { + private static final long serialVersionUID = 1L; + public ProxyProtocolParseException(String message) { super(message); } From 4770613c2b16807857af6bba30bb5e326c726dc3 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 12:03:35 +0100 Subject: [PATCH 19/31] remove unused --- .../airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index 4894791..a20b053 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -30,7 +30,6 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox if (data == null || offset < 0 || length < 0) throw new IllegalArgumentException("Invalid arguments"); if ((length+offset) > data.length) throw new IllegalArgumentException("Invalid offset/length combination with data length"); - int end = offset + length; if (PROTOCOL_SIGNATURE_FIXED_LENGTH > length) throw new ProxyProtocolParseException("Insufficient data for header"); for (int i = 0; i < PROTOCOL_SIGNATURE.length; i++) { From 128bcac9acdc368577f0c390b24d503d2eccf05b Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 16:26:11 +0100 Subject: [PATCH 20/31] add optional TLV parsing flag --- .github/workflows/pull_requests.yml | 2 +- .../proxysocket/core/v2/ProxyProtocolV2Decoder.java | 11 +++++++++-- .../core/v2/ProxyProtocolV2DecoderTest.java | 4 ++-- .../proxysocket/core/v2/ProxyProtocolV2Test.java | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index 91151e0..104896e 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -31,7 +31,7 @@ jobs: - name: Build and test run: | - mvn -B -e -DskipITs=true -DskipIT=true -DskipITsTests=true -DskipIntegrationTests=true -DskipDocker=true -DskipNative=true -DskipExamples=true --fail-at-end clean verify + mvn -B -e --fail-at-end clean verify - name: Upload test reports if: always() diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index a20b053..b93dc15 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -27,6 +27,10 @@ private ProxyProtocolV2Decoder() {} private static final int TLV_HEADER_LEN = 3; public static ProxyHeader parse(byte[] data, int offset, int length) throws ProxyProtocolParseException, IllegalArgumentException { + return parse(data, offset, length, false); + } + + public static ProxyHeader parse(byte[] data, int offset, int length, boolean parseTlvs) throws ProxyProtocolParseException, IllegalArgumentException { if (data == null || offset < 0 || length < 0) throw new IllegalArgumentException("Invalid arguments"); if ((length+offset) > data.length) throw new IllegalArgumentException("Invalid offset/length combination with data length"); @@ -78,8 +82,11 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox pos += addresses.bytesConsumed; } - int tlvLen = Math.max(0, variableLength - (addresses != null ? addresses.bytesConsumed : 0)); - List tlvs = parseTlvs(data, pos, tlvLen); + List tlvs = null; + if (parseTlvs) { + int tlvLen = Math.max(0, variableLength - (addresses != null ? addresses.bytesConsumed : 0)); + tlvs = parseTlvs(data, pos, tlvLen); + } InetSocketAddress src = addresses != null ? addresses.src : null; InetSocketAddress dst = addresses != null ? addresses.dst : null; diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java index 28d5f3c..133c985 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java @@ -110,7 +110,7 @@ void decodeUnspecWithTlvOnly() throws Exception { h[p++] = 0x00; h[p++] = (byte) tlv.length; System.arraycopy(tlv, 0, h, p, tlv.length); - ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length, true); assertEquals(ProxyHeader.AddressFamily.AF_UNSPEC, parsed.getFamily()); assertNotNull(parsed.getTlvs()); assertEquals(1, parsed.getTlvs().size()); @@ -193,7 +193,7 @@ void tlvLengthOverrunIgnored() throws Exception { System.arraycopy(payload, 0, h, p, payload.length); // Parser should handle gracefully without throwing - ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length); + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length, true); assertNotNull(parsed); assertEquals(ProxyHeader.AddressFamily.AF_INET, parsed.getFamily()); } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java index bdae470..e7d0724 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -43,7 +43,7 @@ void parseIPv6UdpWithTlv() throws Exception { .addTlv(0xEA, new byte[]{0x41, 0x42}) // Use non-reserved TLV type .build(); - ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length, true); assertEquals(ProxyHeader.AddressFamily.AF_INET6, parsed.getFamily()); assertEquals(ProxyHeader.TransportProtocol.DGRAM, parsed.getProtocol()); // AWS ProProt adds extra TLVs, find our custom TLV @@ -89,7 +89,7 @@ void proxyUnspecWithTlvOnly() throws Exception { .addTlv(0xEE, new byte[]{0x10, 0x20, 0x30}) .build(); - ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length); + ProxyHeader parsed = ProxyProtocolV2Decoder.parse(header, 0, header.length, true); assertEquals(ProxyHeader.AddressFamily.AF_UNSPEC, parsed.getFamily()); // AWS ProProt adds extra TLVs, find our custom TLV assertTrue(parsed.getTlvs().size() >= 1); From dbfe0f82cab82f4a8846b618205e2597cf5876ed Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 16:52:41 +0100 Subject: [PATCH 21/31] remove uneeded import --- .../net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java | 1 - 1 file changed, 1 deletion(-) diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java index e7d0724..d18cc9f 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -9,7 +9,6 @@ import net.airvantage.proxysocket.core.ProxyProtocolParseException; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; From 8876188810e4461813cf15cec244f03a9e2e8b62 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 17:13:12 +0100 Subject: [PATCH 22/31] remove uneeded import --- .../net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java | 1 - 1 file changed, 1 deletion(-) diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java index d18cc9f..e5d74cd 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Test; import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; From b1ccf69ba0318cd0573b41fed9c8a926da6b90ee Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 17:46:09 +0100 Subject: [PATCH 23/31] remove uneeded import --- .../net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java | 1 - 1 file changed, 1 deletion(-) diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java index e5d74cd..ce25b5d 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Test.java @@ -6,7 +6,6 @@ */ package net.airvantage.proxysocket.core.v2; -import net.airvantage.proxysocket.core.ProxyProtocolParseException; import org.junit.jupiter.api.Test; import java.net.InetSocketAddress; From beb2682754e05344a23ea8ff7e7faee51ce733f4 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Tue, 18 Nov 2025 17:49:14 +0100 Subject: [PATCH 24/31] cleanup null test --- .../cache/ConcurrentMapProxyAddressCache.java | 9 ------ .../ConcurrentMapProxyAddressCacheTest.java | 28 ------------------- 2 files changed, 37 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java index 0d55e5e..49baab0 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java @@ -17,25 +17,16 @@ public final class ConcurrentMapProxyAddressCache implements ProxyAddressCache { @Override public void put(InetSocketAddress clientAddr, InetSocketAddress proxyAddr) { - if (clientAddr == null || proxyAddr == null) { - return; - } map.put(clientAddr, proxyAddr); } @Override public InetSocketAddress get(InetSocketAddress clientAddr) { - if (clientAddr == null) { - return null; - } return map.get(clientAddr); } @Override public void invalidate(InetSocketAddress clientAddr) { - if (clientAddr == null) { - return; - } map.remove(clientAddr); } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java index 27042ce..7ab8a2c 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java @@ -62,34 +62,6 @@ void testPutOverwritesExistingValue() { assertEquals(proxyAddr2, result); } - @Test - void testPutNullClientAddress() { - cache.put(null, proxyAddr1); - // Should not throw exception, just ignore - assertNull(cache.get(clientAddr1)); - } - - @Test - void testPutNullProxyAddress() { - cache.put(clientAddr1, null); - // Should not throw exception, just ignore - assertNull(cache.get(clientAddr1)); - } - - @Test - void testPutBothNull() { - cache.put(null, null); - // Should not throw exception - assertNull(cache.get(clientAddr1)); - } - - @Test - void testGetNullAddress() { - cache.put(clientAddr1, proxyAddr1); - InetSocketAddress result = cache.get(null); - assertNull(result); - } - @Test void testInvalidate() { cache.put(clientAddr1, proxyAddr1); From e05039f464075fb477701be879f8057e64c20baa Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Wed, 19 Nov 2025 10:54:53 +0100 Subject: [PATCH 25/31] rename variable --- .../main/java/net/airvantage/proxysocket/core/v2/Tlv.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java index 7a37f9b..74f4e29 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java @@ -20,8 +20,8 @@ public Tlv(int type, byte[] data, int offset, int length) { @Override public String toString() { - int show = Math.min(value.length, 16); - byte[] head = Arrays.copyOf(value, show); - return "Tlv{" + "type=" + type + ", len=" + value.length + ", head=" + Arrays.toString(head) + (value.length > show ? ", ..." : "") + '}'; + int displayLimit = Math.min(value.length, 16); + byte[] head = Arrays.copyOf(value, displayLimit); + return "Tlv{" + "type=" + type + ", len=" + value.length + ", head=" + Arrays.toString(head) + (value.length > displayLimit ? ", ..." : "") + '}'; } } From e0902bedde4d81875b3c42b3643d93e767c50d99 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Wed, 19 Nov 2025 16:38:33 +0100 Subject: [PATCH 26/31] last cleanups --- .../core/v2/ProxyProtocolV2Decoder.java | 11 +-- .../airvantage/proxysocket/core/v2/Tlv.java | 9 ++- .../ConcurrentMapProxyAddressCacheTest.java | 68 ------------------- 3 files changed, 14 insertions(+), 74 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index b93dc15..13a6bc0 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -199,16 +199,19 @@ private static AddressPair parseUnixAddresses(byte[] data, int pos, int variable return new AddressPair(null, null, UNIX_ADDR_PAIR_LEN); } - private static List parseTlvs(byte[] data, int pos, int tlvLen) { + private static List parseTlvs(byte[] data, int tlvPos, int tlvLen) + throws ProxyProtocolParseException { List tlvs = new ArrayList<>(); - int tlvPos = pos; + int tlvEnd = tlvPos + tlvLen; while (tlvPos + TLV_HEADER_LEN <= tlvEnd) { int type = data[tlvPos++] & 0xFF; int len = ((data[tlvPos++] & 0xFF) << 8) | (data[tlvPos++] & 0xFF); - if (tlvPos + len > tlvEnd) break; - tlvs.add(new Tlv(type, data, tlvPos, len)); + if (tlvPos + len > tlvEnd) { + throw new ProxyProtocolParseException("Truncated TLV in header"); + } + tlvs.add(Tlv.extractTlvFromPacket(type, data, tlvPos, len)); tlvPos += len; } return tlvs; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java index 74f4e29..8a333c4 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java @@ -10,9 +10,14 @@ public final class Tlv { private final int type; private final byte[] value; - public Tlv(int type, byte[] data, int offset, int length) { + public Tlv(int type, byte[] data) { this.type = type; - this.value = Arrays.copyOfRange(data, offset, offset + length); + this.value = data; + } + + public static extractTlvFromPacket(int type, byte[] packet, int offset, int length) { + byte[] data = Arrays.copyOfRange(packet, offset, offset + length); + return new Tlv(type, data); } public int getType() { return type; } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java index 7ab8a2c..0e4b6a9 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java @@ -98,73 +98,5 @@ void testClear() { assertNull(cache.get(clientAddr1)); assertNull(cache.get(clientAddr2)); } - - @Test - void testConcurrentPutAndGet() throws InterruptedException { - int threadCount = 10; - int operationsPerThread = 100; - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - - for (int i = 0; i < threadCount; i++) { - final int threadId = i; - executor.submit(() -> { - try { - for (int j = 0; j < operationsPerThread; j++) { - InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + threadId, 10000 + j); - InetSocketAddress proxyAddr = new InetSocketAddress("10.0.0." + threadId, 443); - cache.put(clientAddr, proxyAddr); - InetSocketAddress retrieved = cache.get(clientAddr); - assertNotNull(retrieved); - assertEquals(proxyAddr, retrieved); - } - } finally { - latch.countDown(); - } - }); - } - - assertTrue(latch.await(10, TimeUnit.SECONDS)); - executor.shutdown(); - assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); - } - - @Test - void testConcurrentInvalidate() throws InterruptedException { - // Pre-populate cache - for (int i = 0; i < 100; i++) { - InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + i, 10000); - InetSocketAddress proxyAddr = new InetSocketAddress("10.0.0." + i, 443); - cache.put(clientAddr, proxyAddr); - } - - int threadCount = 10; - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - - for (int i = 0; i < threadCount; i++) { - final int threadId = i; - executor.submit(() -> { - try { - for (int j = 0; j < 10; j++) { - InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + (threadId * 10 + j), 10000); - cache.invalidate(clientAddr); - } - } finally { - latch.countDown(); - } - }); - } - - assertTrue(latch.await(10, TimeUnit.SECONDS)); - executor.shutdown(); - assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); - - // Verify all addresses were invalidated - for (int i = 0; i < 100; i++) { - InetSocketAddress clientAddr = new InetSocketAddress("192.168.1." + i, 10000); - assertNull(cache.get(clientAddr)); - } - } } From 30ef6056f012c6ec4d18848035921402a0b0b37c Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Wed, 19 Nov 2025 16:41:00 +0100 Subject: [PATCH 27/31] cleanups --- .../main/java/net/airvantage/proxysocket/core/v2/Tlv.java | 2 +- .../core/cache/ConcurrentMapProxyAddressCacheTest.java | 8 -------- .../proxysocket/core/v2/ProxyProtocolV2DecoderTest.java | 4 +--- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java index 8a333c4..6698de5 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java @@ -15,7 +15,7 @@ public Tlv(int type, byte[] data) { this.value = data; } - public static extractTlvFromPacket(int type, byte[] packet, int offset, int length) { + public static Tlv extractTlvFromPacket(int type, byte[] packet, int offset, int length) { byte[] data = Arrays.copyOfRange(packet, offset, offset + length); return new Tlv(type, data); } diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java index 0e4b6a9..8f7cf02 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java @@ -80,14 +80,6 @@ void testInvalidateNonExistentAddress() { assertNull(cache.get(clientAddr1)); } - @Test - void testInvalidateNullAddress() { - cache.put(clientAddr1, proxyAddr1); - cache.invalidate(null); - // Should not throw exception, should not affect existing entries - assertEquals(proxyAddr1, cache.get(clientAddr1)); - } - @Test void testClear() { cache.put(clientAddr1, proxyAddr1); diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java index 133c985..03ed2b0 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2DecoderTest.java @@ -193,9 +193,7 @@ void tlvLengthOverrunIgnored() throws Exception { System.arraycopy(payload, 0, h, p, payload.length); // Parser should handle gracefully without throwing - ProxyHeader parsed = ProxyProtocolV2Decoder.parse(h, 0, h.length, true); - assertNotNull(parsed); - assertEquals(ProxyHeader.AddressFamily.AF_INET, parsed.getFamily()); + assertThrows(ProxyProtocolParseException.class, () -> ProxyProtocolV2Decoder.parse(h, 0, h.length, true)); } } From 3098cb2b57ce4f987e424f48737eae56149c15be Mon Sep 17 00:00:00 2001 From: bplessis14821 Date: Thu, 20 Nov 2025 11:42:54 +0100 Subject: [PATCH 28/31] remove suggested unused imports Co-authored-by: cthirouin <113358856+cthirouin-swi@users.noreply.github.com> --- .../proxysocket/core/v2/ProxyProtocolV2Decoder.java | 1 - .../core/cache/ConcurrentMapProxyAddressCacheTest.java | 4 ---- 2 files changed, 5 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index 13a6bc0..4fd6989 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -8,7 +8,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; diff --git a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java index 8f7cf02..0875bae 100644 --- a/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java +++ b/proxy-socket-core/src/test/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCacheTest.java @@ -8,10 +8,6 @@ import org.junit.jupiter.api.Test; import java.net.InetSocketAddress; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; From 175c850e9da8e63de56ed81f71d9c8d79a08d164 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Thu, 20 Nov 2025 15:17:51 +0100 Subject: [PATCH 29/31] expand the throw to a block --- .../core/v2/ProxyProtocolV2Decoder.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index 4fd6989..9c98f77 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -30,10 +30,17 @@ public static ProxyHeader parse(byte[] data, int offset, int length) throws Prox } public static ProxyHeader parse(byte[] data, int offset, int length, boolean parseTlvs) throws ProxyProtocolParseException, IllegalArgumentException { - if (data == null || offset < 0 || length < 0) throw new IllegalArgumentException("Invalid arguments"); - if ((length+offset) > data.length) throw new IllegalArgumentException("Invalid offset/length combination with data length"); + if (data == null || offset < 0 || length < 0) { + throw new IllegalArgumentException("Invalid arguments"); + } + + if ((length+offset) > data.length) { + throw new IllegalArgumentException("Invalid offset/length combination with data length"); + } - if (PROTOCOL_SIGNATURE_FIXED_LENGTH > length) throw new ProxyProtocolParseException("Insufficient data for header"); + if (PROTOCOL_SIGNATURE_FIXED_LENGTH > length) { + throw new ProxyProtocolParseException("Insufficient data for header"); + } for (int i = 0; i < PROTOCOL_SIGNATURE.length; i++) { if (data[offset + i] != PROTOCOL_SIGNATURE[i]) { @@ -46,8 +53,12 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par // Byte 13: version/command int verCmd = data[pos++] & 0xFF; int version = (verCmd >> 4) & 0x0F; - if (version != 2) throw new ProxyProtocolParseException("Invalid version"); int cmd = verCmd & 0x0F; + + if (version != 2) { + throw new ProxyProtocolParseException("Invalid version"); + } + ProxyHeader.Command command; switch (cmd) { case 0x00: @@ -73,7 +84,9 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par // Check if we have enough data for the header int headerLen = PROTOCOL_SIGNATURE_FIXED_LENGTH + variableLength; - if (headerLen > length) throw new ProxyProtocolParseException("Insufficient data for header"); + if (headerLen > length) { + throw new ProxyProtocolParseException("Insufficient data for header"); + } AddressPair addresses = null; if (af != ProxyHeader.AddressFamily.AF_UNSPEC) { From 77c6d8bddd15f50ee8d038ae76e5c3b8dff72653 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Fri, 21 Nov 2025 19:32:29 +0100 Subject: [PATCH 30/31] Take in account simon's review --- .../proxysocket/core/ProxyAddressCache.java | 4 +-- .../core/ProxyProtocolException.java | 4 +-- .../core/ProxyProtocolMetricsListener.java | 4 +-- .../core/ProxyProtocolParseException.java | 4 +-- .../cache/ConcurrentMapProxyAddressCache.java | 4 +-- .../proxysocket/core/v2/ProxyHeader.java | 4 +-- .../core/v2/ProxyProtocolV2Decoder.java | 31 ++++++++----------- .../airvantage/proxysocket/core/v2/Tlv.java | 4 +-- 8 files changed, 27 insertions(+), 32 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java index 08df66e..f691223 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyAddressCache.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java index 17fbd89..0b80c52 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolException.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java index e6587d8..11ca5ab 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolMetricsListener.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java index 52775c0..47e6783 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/ProxyProtocolParseException.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java index 49baab0..4060190 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/cache/ConcurrentMapProxyAddressCache.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core.cache; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java index dd8c086..5cedc60 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyHeader.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core.v2; diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index 9c98f77..f63ae4e 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core.v2; @@ -90,7 +90,12 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par AddressPair addresses = null; if (af != ProxyHeader.AddressFamily.AF_UNSPEC) { - addresses = parseAddresses(data, pos, af, variableLength); + addresses = switch (af) { + case AF_INET -> parseIPv4Addresses(data, pos, variableLength); + case AF_INET6 -> parseIPv6Addresses(data, pos, variableLength); + case AF_UNIX -> parseUnixAddresses(data, pos, variableLength); + default -> throw new ProxyProtocolParseException("Invalid address family"); + }; pos += addresses.bytesConsumed; } @@ -139,17 +144,7 @@ private static class AddressPair { } } - private static AddressPair parseAddresses(byte[] data, int currentPosition, ProxyHeader.AddressFamily af, int variableLength) - throws ProxyProtocolParseException { - return switch (af) { - case AF_INET -> parseIPv4Addresses(data, currentPosition, variableLength); - case AF_INET6 -> parseIPv6Addresses(data, currentPosition, variableLength); - case AF_UNIX -> parseUnixAddresses(data, currentPosition, variableLength); - default -> throw new ProxyProtocolParseException("Invalid address family"); - }; - } - - private static int IPV4_ADDR_PAIR_LEN = 2*(IPV4_ADDR_LEN + PORT_LEN); + private static final int IPV4_ADDR_PAIR_LEN = 2*(IPV4_ADDR_LEN + PORT_LEN); private static AddressPair parseIPv4Addresses(byte[] data, int pos, int variableLength) throws ProxyProtocolParseException { if (variableLength < IPV4_ADDR_PAIR_LEN) { @@ -166,12 +161,12 @@ private static AddressPair parseIPv4Addresses(byte[] data, int pos, int variable } int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + int dp = ((data[pos++] & 0xFF) << 8) | (data[pos ] & 0xFF); return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), IPV4_ADDR_PAIR_LEN); } - private static int IPV6_ADDR_PAIR_LEN = 2*(IPV6_ADDR_LEN + PORT_LEN); + private static final int IPV6_ADDR_PAIR_LEN = 2*(IPV6_ADDR_LEN + PORT_LEN); private static AddressPair parseIPv6Addresses(byte[] data, int pos, int variableLength) throws ProxyProtocolParseException { if (variableLength < IPV6_ADDR_PAIR_LEN) { @@ -193,12 +188,12 @@ private static AddressPair parseIPv6Addresses(byte[] data, int pos, int variable pos += 2*IPV6_ADDR_LEN; int sp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); - int dp = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); + int dp = ((data[pos++] & 0xFF) << 8) | (data[pos ] & 0xFF); return new AddressPair(new InetSocketAddress(s, sp), new InetSocketAddress(d, dp), IPV6_ADDR_PAIR_LEN); } - private static int UNIX_ADDR_PAIR_LEN = 2*UNIX_ADDR_LEN; + private static final int UNIX_ADDR_PAIR_LEN = 2*UNIX_ADDR_LEN; private static AddressPair parseUnixAddresses(byte[] data, int pos, int variableLength) throws ProxyProtocolParseException { if (variableLength < UNIX_ADDR_PAIR_LEN) { diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java index 6698de5..1dd3d2c 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/Tlv.java @@ -1,5 +1,5 @@ -/* - * MIT License +/** + * BSD-3-Clause License. * Copyright (c) 2025 Semtech */ package net.airvantage.proxysocket.core.v2; From 236e1ab2bab2d8a5e09eb0de9c978b1e61c6feb0 Mon Sep 17 00:00:00 2001 From: Benoit Plessis Date: Mon, 24 Nov 2025 11:46:59 +0100 Subject: [PATCH 31/31] Use java 16 records --- .../core/v2/ProxyProtocolV2Decoder.java | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java index f63ae4e..95f0edd 100644 --- a/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java +++ b/proxy-socket-core/src/main/java/net/airvantage/proxysocket/core/v2/ProxyProtocolV2Decoder.java @@ -5,6 +5,10 @@ package net.airvantage.proxysocket.core.v2; import net.airvantage.proxysocket.core.ProxyProtocolParseException; +import net.airvantage.proxysocket.core.v2.ProxyHeader.TransportProtocol; +import net.airvantage.proxysocket.core.v2.ProxyHeader.AddressFamily; +import net.airvantage.proxysocket.core.v2.ProxyHeader.Command; + import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; @@ -25,6 +29,8 @@ private ProxyProtocolV2Decoder() {} private static final int PORT_LEN = 2; private static final int TLV_HEADER_LEN = 3; + private record AddressPair(InetSocketAddress src, InetSocketAddress dst, int bytesConsumed) {} + public static ProxyHeader parse(byte[] data, int offset, int length) throws ProxyProtocolParseException, IllegalArgumentException { return parse(data, offset, length, false); } @@ -59,13 +65,13 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par throw new ProxyProtocolParseException("Invalid version"); } - ProxyHeader.Command command; + Command command; switch (cmd) { case 0x00: // Early return for LOCAL command - return new ProxyHeader(ProxyHeader.Command.LOCAL, ProxyHeader.AddressFamily.AF_UNSPEC, ProxyHeader.TransportProtocol.UNSPEC, null, null, null, PROTOCOL_SIGNATURE_FIXED_LENGTH); + return new ProxyHeader(Command.LOCAL, AddressFamily.AF_UNSPEC, TransportProtocol.UNSPEC, null, null, null, PROTOCOL_SIGNATURE_FIXED_LENGTH); case 0x01: - command = ProxyHeader.Command.PROXY; + command = Command.PROXY; break; default: throw new ProxyProtocolParseException("Invalid command"); @@ -76,8 +82,8 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par int fam = famProto & 0xF0; int proto = famProto & 0x0F; - ProxyHeader.AddressFamily af = parseAddressFamily(fam); - ProxyHeader.TransportProtocol tp = parseTransportProtocol(proto); + AddressFamily af = parseAddressFamily(fam); + TransportProtocol tp = parseTransportProtocol(proto); // Byte 15, 16: Length of address part of the header, including TLVs int variableLength = ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); @@ -89,7 +95,7 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par } AddressPair addresses = null; - if (af != ProxyHeader.AddressFamily.AF_UNSPEC) { + if (af != AddressFamily.AF_UNSPEC) { addresses = switch (af) { case AF_INET -> parseIPv4Addresses(data, pos, variableLength); case AF_INET6 -> parseIPv6Addresses(data, pos, variableLength); @@ -111,39 +117,27 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par return new ProxyHeader(command, af, tp, src, dst, tlvs, headerLen); } - private static ProxyHeader.AddressFamily parseAddressFamily(int fam) + private static AddressFamily parseAddressFamily(int fam) throws ProxyProtocolParseException { return switch (fam) { - case 0x00 -> ProxyHeader.AddressFamily.AF_UNSPEC; - case 0x10 -> ProxyHeader.AddressFamily.AF_INET; - case 0x20 -> ProxyHeader.AddressFamily.AF_INET6; - case 0x30 -> ProxyHeader.AddressFamily.AF_UNIX; + case 0x00 -> AddressFamily.AF_UNSPEC; + case 0x10 -> AddressFamily.AF_INET; + case 0x20 -> AddressFamily.AF_INET6; + case 0x30 -> AddressFamily.AF_UNIX; default -> throw new ProxyProtocolParseException("Invalid address family"); }; } - private static ProxyHeader.TransportProtocol parseTransportProtocol(int proto) + private static TransportProtocol parseTransportProtocol(int proto) throws ProxyProtocolParseException { return switch (proto) { - case 0x00 -> ProxyHeader.TransportProtocol.UNSPEC; - case 0x01 -> ProxyHeader.TransportProtocol.STREAM; - case 0x02 -> ProxyHeader.TransportProtocol.DGRAM; + case 0x00 -> TransportProtocol.UNSPEC; + case 0x01 -> TransportProtocol.STREAM; + case 0x02 -> TransportProtocol.DGRAM; default -> throw new ProxyProtocolParseException("Invalid transport protocol"); }; } - private static class AddressPair { - final InetSocketAddress src; - final InetSocketAddress dst; - final int bytesConsumed; - - AddressPair(InetSocketAddress src, InetSocketAddress dst, int bytesConsumed) { - this.src = src; - this.dst = dst; - this.bytesConsumed = bytesConsumed; - } - } - private static final int IPV4_ADDR_PAIR_LEN = 2*(IPV4_ADDR_LEN + PORT_LEN); private static AddressPair parseIPv4Addresses(byte[] data, int pos, int variableLength) throws ProxyProtocolParseException {