Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
90cf559
RAV-2958 - Add UDP Socket support
bplessis-swi Oct 31, 2025
e250c97
disable integration test with nginx doesn't work
bplessis-swi Nov 4, 2025
00f40ee
add subnet predicate helper
bplessis-swi Nov 26, 2025
fadd09d
process review step 2
bplessis-swi Nov 27, 2025
07ee29c
fix tests
bplessis-swi Nov 27, 2025
d582239
Fix the handling on local mode in AwsProxyEncoderHelper
bplessis-swi Nov 27, 2025
9977227
remove testcontainers
bplessis-swi Nov 27, 2025
5998205
try to factor-in
bplessis-swi Nov 27, 2025
ddd5598
naming
bplessis-swi Nov 28, 2025
c1093d4
naming
bplessis-swi Nov 28, 2025
322978c
Merge branch 'main' into RAV-2958_add_udp_socket
bplessis-swi Nov 28, 2025
022e272
Switch to warn for packets dropped due to cache expiration
bplessis-swi Nov 28, 2025
d9f0dc5
fix name
bplessis-swi Nov 28, 2025
59c285f
Update proxy-socket-udp/src/test/java/net/airvantage/proxysocket/udp/…
bplessis-swi Dec 1, 2025
7ec9ee9
review comments
bplessis-swi Dec 1, 2025
26bd11a
enforce address family and protocol in the build phase
bplessis-swi Dec 1, 2025
15bcf03
review comments
bplessis-swi Dec 1, 2025
68f3d59
fix licences
bplessis-swi Dec 1, 2025
8c187da
fix licences
bplessis-swi Dec 1, 2025
34cfc9a
process review
bplessis-swi Dec 1, 2025
c2bc5ea
revert throw
bplessis-swi Dec 1, 2025
09b7c43
reduce try block
bplessis-swi Dec 1, 2025
0eb3d7b
convert to parametized tests
bplessis-swi Dec 1, 2025
1923df8
currently unused
bplessis-swi Dec 1, 2025
76664a7
add a little documentation about using hostnames
bplessis-swi Dec 1, 2025
4358848
reuse constructors
bplessis-swi Dec 1, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

# maven build directories
target/*
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

<modules>
<module>proxy-socket-core</module>
<module>proxy-socket-udp</module>
<module>proxy-socket-guava</module>
</modules>

Expand Down
18 changes: 18 additions & 0 deletions proxy-socket-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@
</dependency>
</dependencies>

<build>
<plugins>
<!-- Create test-jar to share test utilities with other modules -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>


Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package net.airvantage.proxysocket.core;

import net.airvantage.proxysocket.core.v2.ProxyHeader;

import java.net.InetAddress;
import java.net.InetSocketAddress;

/**
Expand All @@ -16,4 +18,7 @@ default void onHeaderParsed(ProxyHeader header) {}
default void onParseError(Exception e) {}
default void onCacheHit(InetSocketAddress client) {}
default void onCacheMiss(InetSocketAddress client) {}
default void onUntrustedProxy(InetAddress proxy) {}
default void onTrustedProxy(InetAddress proxy) {}
default void onLocal(InetAddress proxy) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,11 @@ public static ProxyHeader parse(byte[] data, int offset, int length, boolean par
throw new ProxyProtocolParseException("Invalid version");
}

Command command;
switch (cmd) {
case 0x00:
// Early return for LOCAL command
return new ProxyHeader(Command.LOCAL, AddressFamily.AF_UNSPEC, TransportProtocol.UNSPEC, null, null, null, PROTOCOL_SIGNATURE_FIXED_LENGTH);
case 0x01:
command = Command.PROXY;
break;
default:
throw new ProxyProtocolParseException("Invalid command");
}
Command command = switch (cmd) {
case 0x00 -> Command.LOCAL;
case 0x01 -> Command.PROXY;
default -> throw new ProxyProtocolParseException("Invalid command");
};

// Byte 14: address family and protocol
int famProto = data[pos++] & 0xFF;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* BSD-3-Clause License.
* Copyright (c) 2025 Semtech
*/
package net.airvantage.proxysocket.tools;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.function.Predicate;

/**
* Predicate compatible class that tests whether an InetSocketAddress belongs to a given subnet (CIDR).
* Supports both IPv4 and IPv6 CIDR notation.
*
* <p>Example usage:
* <pre>
* // Single subnet
* Predicate<InetSocketAddress> predicate = new SubnetPredicate("10.0.0.0/8");
*
* // Multiple subnets
* Predicate<InetSocketAddress> predicate =
* new SubnetPredicate("10.0.0.0/8")
* .or(new SubnetPredicate("192.168.0.0/16"))
* .or(new SubnetPredicate("2001:db8::/32"))
* );
* </pre>
*
* Note: it's possible to create a SubnetPredicate with a hostname instead of an IP address,
* the address will be resolved to an IP address using InetAddress.getByName(hostname).
* If the hostname is not resolvable, an IllegalArgumentException will be thrown.
* But if the hostname resolves to a mix of IPv4/IPv6 addresses or multiple addresses,
* the predicate will only match the first address found.
*
* Thread-safety: This class is immutable and thread-safe.
*/
public class SubnetPredicate implements Predicate<InetSocketAddress> {
private final byte[] networkAddress;
private final int prefixLength;
private final int addressLength; // 4 for IPv4, 16 for IPv6

/**
* Creates a predicate for the given CIDR subnet.
*
* @param cidr CIDR notation string (e.g., "10.0.0.0/8" or "2001:db8::/32")
* @throws IllegalArgumentException if the CIDR notation is invalid
*/
public SubnetPredicate(String cidr) {
if (cidr == null || cidr.isEmpty()) {
throw new IllegalArgumentException("CIDR notation cannot be null or empty");
}

int slashIndex = cidr.indexOf('/');
if (slashIndex == -1) {
throw new IllegalArgumentException("Invalid CIDR notation: missing '/' separator");
}

String addressPart = cidr.substring(0, slashIndex);
String prefixLengthPart = cidr.substring(slashIndex + 1);

try {
this.prefixLength = Integer.parseInt(prefixLengthPart);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid prefix length: " + prefixLengthPart, e);
}

byte[] rawAddress;
try {
InetAddress addr = InetAddress.getByName(addressPart);
rawAddress = addr.getAddress();
} catch (UnknownHostException e) {
throw new IllegalArgumentException("Invalid IP address: " + addressPart, e);
}

this.addressLength = rawAddress.length;

// Validate prefix length
int maxPrefixLength = addressLength * 8; // converts address length to bits
if (prefixLength < 0 || prefixLength > maxPrefixLength) {
throw new IllegalArgumentException(
"Invalid prefix length " + prefixLength + " for address type (must be 0-" + maxPrefixLength + ")"
);
}

// Apply the mask to the network address to normalize it
this.networkAddress = applyMask(rawAddress);
}

/**
* Tests whether the given socket address belongs to this subnet.
*
* @param socketAddress the socket address to test
* @return true if the address is in this subnet, false otherwise
*/
@Override
public boolean test(InetSocketAddress socketAddress) {
if (socketAddress == null) {
return false;
}

InetAddress address = socketAddress.getAddress();
if (address == null) {
return false;
}

byte[] testAddress = address.getAddress();

// Different address families don't match
if (testAddress.length != addressLength) {
return false;
}

byte[] maskedTestAddress = applyMask(testAddress);

// Compare network portions
for (int i = 0; i < networkAddress.length; i++) {
if (networkAddress[i] != maskedTestAddress[i]) {
return false;
}
}

return true;
}

/**
* Applies a subnet mask to an IP address.
*
* @param address the raw IP address bytes
* @return the masked address bytes
*/
private byte[] applyMask(byte[] address) {
byte[] result = new byte[address.length];

int fullBytes = prefixLength / 8;
int remainingBits = prefixLength % 8;

// Copy the full bytes
System.arraycopy(address, 0, result, 0, fullBytes);

// Apply mask to the partial byte if any
if (remainingBits > 0) {
int mask = 0xFF << (8 - remainingBits);
result[fullBytes] = (byte) (address[fullBytes] & mask);
}

// Remaining bytes are already 0
return result;
}

@Override
public String toString() {
try {
InetAddress addr = InetAddress.getByAddress(networkAddress);
return addr.getHostAddress() + "/" + prefixLength;
} catch (UnknownHostException e) {
return "SubnetPredicate[invalid]";
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* BSD-3-Clause License.
* Copyright (c) 2025 Semtech
*/
package net.airvantage.proxysocket.core.cache;
package net.airvantage.proxysocket.tools.cache;

import net.airvantage.proxysocket.core.ProxyAddressCache;
import java.net.InetSocketAddress;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* MIT License
/**
* BSD-3-Clause License.
* Copyright (c) 2025 Semtech
*
* Helper class to encode PROXY protocol v2 headers using AWS ProProt library.
Expand Down Expand Up @@ -28,9 +28,10 @@ public final class AwsProxyEncoderHelper {
private final Header header = new Header();

public AwsProxyEncoderHelper command(ProxyHeader.Command cmd) {
this.command = cmd == ProxyHeader.Command.LOCAL
? ProxyProtocolSpec.Command.LOCAL
: ProxyProtocolSpec.Command.PROXY;
this.command = switch (cmd) {
case LOCAL -> ProxyProtocolSpec.Command.LOCAL;
case PROXY -> ProxyProtocolSpec.Command.PROXY;
};
return this;
}

Expand Down Expand Up @@ -73,16 +74,10 @@ public AwsProxyEncoderHelper addTlv(int type, byte[] value) {

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 (command != ProxyProtocolSpec.Command.LOCAL) {
header.setAddressFamily(family);
header.setTransportProtocol(protocol);
if (source != null) {
header.setSrcAddress(source.getAddress().getAddress());
header.setSrcPort(source.getPort());
Expand All @@ -92,6 +87,12 @@ public byte[] build() throws IOException {
header.setDstAddress(destination.getAddress().getAddress());
header.setDstPort(destination.getPort());
}
} else {
// Spec clearly state that for LOCAL command, we
// 1. must discard the protocol block including the family and
// 2. \x00 is expected to be used for the protocol field.
header.setAddressFamily(ProxyProtocolSpec.AddressFamily.AF_UNSPEC);
header.setTransportProtocol(ProxyProtocolSpec.TransportProtocol.UNSPEC);
}

ByteArrayOutputStream out = new ByteArrayOutputStream();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* MIT License
/**
* BSD-3-Clause License.
* Copyright (c) 2025 Semtech

*
* Validation of ProxyProtocolV2Decoder against hardcoded headers for known cases
*/
package net.airvantage.proxysocket.core.v2;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* MIT License
/**
* BSD-3-Clause License.
* Copyright (c) 2025 Semtech

*
* Validation of ProxyProtocolV2Decoder using AWS ProProt library
*/
package net.airvantage.proxysocket.core.v2;
Expand Down
Loading
Loading