Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/pull_requests.yml
Original file line number Diff line number Diff line change
@@ -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: [ '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/**
695 changes: 21 additions & 674 deletions LICENSE

Large diffs are not rendered by default.

67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,65 @@
# 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

Reference: [HAProxy Proxy Protocol Specifications](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)

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`.


99 changes: 99 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>net.airvantage</groupId>
<artifactId>proxysocket-java</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>

<description>ProxyProtocol Java implementation.</description>

<properties>

<!-- Dependency versions -->
<junit.version>5.10.3</junit.version>
<guava.version>33.3.1-jre</guava.version>

</properties>

<modules>
<module>proxy-socket-core</module>
<module>proxy-socket-udp</module>
<!-- <module>proxy-socket-tcp</module>
<module>proxy-socket-guava</module>
<module>proxy-socket-examples</module> -->
</modules>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>2.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<encoding>UTF-8</encoding>
<release>17</release>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.10</version>
<configuration>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>

</plugins>
</build>
</project>
31 changes: 31 additions & 0 deletions proxy-socket-core/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.airvantage</groupId>
<artifactId>proxysocket-java</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>proxy-socket-core</artifactId>
<name>Proxy Protocol - Core</name>
<packaging>jar</packaging>

<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>

<dependencies>
<!-- Core must have zero required dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>


Original file line number Diff line number Diff line change
@@ -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();
}



Original file line number Diff line number Diff line change
@@ -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);
}
}



Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}



Original file line number Diff line number Diff line change
@@ -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<InetSocketAddress, InetSocketAddress> 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();
}
}



Loading
Loading