Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ public boolean isUseTokens() {
return configFile.isTokenSupport();
}

@Override
public java.util.Set<String> getTrustedProxyIps() {
return new java.util.HashSet<String>(getConfigFile().getData().getStringList("TrustedProxyIps"));
}

@Override
public ThrottleConfig getThrottleConfig() {
ConfigurationSection root = getConfigFile().getData().getConfigurationSection("ConnectionThrottle");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ public boolean isUseTokens() {
}

@Override
public java.util.Set<String> getTrustedProxyIps() {
List<String> ips = getConfig().getData().getStringList("TrustedProxyIps");
return ips == null ? Collections.<String>emptySet() : new HashSet<String>(ips);
}

@Override
public ThrottleConfig getThrottleConfig() {
Configuration root = getConfig().getData().getSection("ConnectionThrottle");

Expand Down
111 changes: 111 additions & 0 deletions VotifierPlus/src/main/java/com/vexsoftware/votifier/net/IpLiteral.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.vexsoftware.votifier.net;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

/** Parses address literals without resolving host names or accepting scoped IPv6 addresses. */
final class IpLiteral {
private IpLiteral() {
}

static byte[] parse(String value, int family) throws InvalidVoteException {
if (value == null || value.isEmpty()) {
throw new InvalidVoteException("Invalid PROXY address literal");
}
if (family == 4) {
String[] parts = value.split("\\.", -1);
if (parts.length != 4) {
throw new InvalidVoteException("Invalid PROXY IPv4 literal");
}
byte[] address = new byte[4];
for (int i = 0; i < 4; i++) {
String part = parts[i];
if (part.isEmpty() || part.length() > 3 || (part.length() > 1 && part.charAt(0) == '0')) {
throw new InvalidVoteException("Invalid PROXY IPv4 literal");
}
int octet = 0;
for (int j = 0; j < part.length(); j++) {
char c = part.charAt(j);
if (c < '0' || c > '9') {
throw new InvalidVoteException("Invalid PROXY IPv4 literal");
}
octet = octet * 10 + c - '0';
}
if (octet > 255) {
throw new InvalidVoteException("Invalid PROXY IPv4 literal");
}
address[i] = (byte) octet;
}
return address;
}
if (family != 6 || value.indexOf(':') < 0) {
throw new InvalidVoteException("Invalid PROXY IPv6 literal");
}
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || c == ':' || c == '.')) {
throw new InvalidVoteException("Invalid PROXY IPv6 literal");
}
}
return parseIpv6(value);
}

private static byte[] parseIpv6(String value) throws InvalidVoteException {
String[] halves = value.split("::", -1);
if (halves.length > 2) throw new InvalidVoteException("Invalid PROXY IPv6 literal");

List<Integer> left = parseIpv6Half(halves[0], halves.length == 1);
List<Integer> right = halves.length == 2 ? parseIpv6Half(halves[1], true) : List.of();
int omitted = 8 - left.size() - right.size();
if (halves.length == 1 ? omitted != 0 : omitted < 1) {
throw new InvalidVoteException("Invalid PROXY IPv6 literal");
}

byte[] address = new byte[16];
int index = 0;
for (int group : left) index = writeGroup(address, index, group);
index += omitted * 2;
for (int group : right) index = writeGroup(address, index, group);
return address;
}

private static List<Integer> parseIpv6Half(String half, boolean mayEndWithIpv4) throws InvalidVoteException {
List<Integer> groups = new ArrayList<>();
if (half.isEmpty()) return groups;
String[] parts = half.split(":", -1);
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
if (part.isEmpty()) throw new InvalidVoteException("Invalid PROXY IPv6 literal");
if (part.indexOf('.') >= 0) {
if (!mayEndWithIpv4 || i != parts.length - 1) {
throw new InvalidVoteException("Invalid PROXY IPv6 literal");
}
byte[] ipv4 = parse(part, 4);
groups.add((ipv4[0] & 0xFF) << 8 | ipv4[1] & 0xFF);
groups.add((ipv4[2] & 0xFF) << 8 | ipv4[3] & 0xFF);
continue;
}
if (part.length() > 4) throw new InvalidVoteException("Invalid PROXY IPv6 literal");
int group = 0;
for (int j = 0; j < part.length(); j++) group = group * 16 + Character.digit(part.charAt(j), 16);
groups.add(group);
}
return groups;
}

private static int writeGroup(byte[] address, int index, int group) {
address[index++] = (byte) (group >>> 8);
address[index++] = (byte) group;
return index;
}

static String format(byte[] address) throws InvalidVoteException {
try {
return InetAddress.getByAddress(address).getHostAddress();
} catch (UnknownHostException ex) {
throw new InvalidVoteException("Invalid PROXY address length");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.PushbackInputStream;
import java.net.InetAddress;
import java.net.Inet6Address;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import lombok.Getter;
Expand All @@ -26,6 +30,8 @@ public class ProxyHeaderProcessor {
private static final int MAX_CONNECT_HEADER_BYTES = 32768;
private static final int HEADER_READ_TIMEOUT_MILLIS = 5000;
private static final int DISCARD_BUFFER_BYTES = 1024;
private static final int PROXY_V2_IPV4_BYTES = 12;
private static final int PROXY_V2_IPV6_BYTES = 36;

private static final byte[] PROXY_V1_PREFIX = "PROXY".getBytes(StandardCharsets.US_ASCII);
private static final byte[] CONNECT_PREFIX = "CONNECT".getBytes(StandardCharsets.US_ASCII);
Expand Down Expand Up @@ -73,18 +79,12 @@ private ProxyHeaderResult processWithDeadline(PushbackInputStream in, BufferedWr
if (prefix[0] == PROXY_V1_PREFIX[0]) {
bytesRead = readPrefix(in, prefix, PROXY_V1_PREFIX.length, bytesRead, socket, deadlineNanos);
if (bytesRead == PROXY_V1_PREFIX.length && startsWith(prefix, bytesRead, PROXY_V1_PREFIX)) {
requireTrustedPeer(receiver, socket);
in.unread(prefix, 0, bytesRead);
String proxyHeader = readLine(in, socket, deadlineNanos, MAX_PROXY_V1_HEADER_BYTES, null,
"PROXY protocol v1 header exceeds " + MAX_PROXY_V1_HEADER_BYTES + " bytes");
"PROXY protocol v1 header exceeds " + MAX_PROXY_V1_HEADER_BYTES + " bytes", true);
receiver.debug("Discarded PROXY (v1) header (" + proxyHeader.length() + " chars)");

String[] parts = proxyHeader.split("\\s+");
if (parts.length >= 3) {
String srcIp = parts[2].trim();
if (!srcIp.isEmpty()) {
result.setRealIp(srcIp);
}
}
parseV1(proxyHeader, result);
return result;
}
}
Expand All @@ -102,7 +102,7 @@ private ProxyHeaderResult processWithDeadline(PushbackInputStream in, BufferedWr
while (true) {
String line = readLine(in, socket, deadlineNanos, MAX_CONNECT_LINE_BYTES, totalHeaderBytes,
"HTTP CONNECT header line exceeds " + MAX_CONNECT_LINE_BYTES + " bytes");
if (line.isEmpty()) {
if (line.trim().isEmpty()) {
break;
}
if (++headerCount > MAX_CONNECT_HEADERS) {
Expand All @@ -124,8 +124,9 @@ private ProxyHeaderResult processWithDeadline(PushbackInputStream in, BufferedWr
throw new InvalidVoteException("Incomplete PROXY protocol v2 header");
}
if (bytesRead == 16 && startsWith(prefix, bytesRead, PROXY_V2_SIGNATURE)) {
requireTrustedPeer(receiver, socket);
int addressLength = ((prefix[14] & 0xFF) << 8) | (prefix[15] & 0xFF);
discardFully(in, addressLength, socket, deadlineNanos);
parseV2(in, prefix, addressLength, socket, deadlineNanos, result);
receiver.debug("Discarded PROXY protocol v2 header (" + (16 + addressLength) + " bytes)");
return result;
}
Expand All @@ -135,6 +136,117 @@ private ProxyHeaderResult processWithDeadline(PushbackInputStream in, BufferedWr
return result;
}

private void requireTrustedPeer(VoteReceiver receiver, Socket socket) throws InvalidVoteException {
InetAddress peer = socket == null ? null : socket.getInetAddress();
Set<String> configured = receiver.getTrustedProxyIps();
// Unscoped text cannot distinguish the interface of a link-local IPv6 peer.
// Fail closed instead of trusting the same address bytes on another interface.
if (peer instanceof Inet6Address ipv6 && ipv6.isLinkLocalAddress()) {
throw new InvalidVoteException("Link-local IPv6 peers cannot be trusted for PROXY protocol");
}
if (peer != null && configured != null) {
for (String literal : configured) {
if (literal == null) {
continue;
}
try {
if (Arrays.equals(peer.getAddress(), IpLiteral.parse(literal.trim(), peer.getAddress().length == 4 ? 4 : 6))) {
Comment thread
BenCodez marked this conversation as resolved.
return;
}
} catch (InvalidVoteException ignored) {
// Invalid configuration entries cannot grant trust.
}
}
}
throw new InvalidVoteException("PROXY protocol header from untrusted socket peer");
}

private void parseV1(String header, ProxyHeaderResult result) throws InvalidVoteException {
String[] parts = header.split(" ", -1);
if (parts.length >= 2 && "PROXY".equals(parts[0]) && "UNKNOWN".equals(parts[1])) {
return;
}
if (parts.length != 6 || !"PROXY".equals(parts[0])) {
throw new InvalidVoteException("Invalid PROXY protocol v1 header");
}
int family;
if ("TCP4".equals(parts[1])) {
family = 4;
} else if ("TCP6".equals(parts[1])) {
family = 6;
} else {
throw new InvalidVoteException("Unsupported PROXY protocol v1 family");
}
byte[] source = IpLiteral.parse(parts[2], family);
IpLiteral.parse(parts[3], family);
parsePort(parts[4]);
parsePort(parts[5]);
result.setRealIp(IpLiteral.format(source));
}

private void parsePort(String value) throws InvalidVoteException {
if (value.isEmpty() || value.length() > 5) {
throw new InvalidVoteException("Invalid PROXY port");
}
int port = 0;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c < '0' || c > '9') {
throw new InvalidVoteException("Invalid PROXY port");
}
port = port * 10 + c - '0';
}
if (port > 65535) {
throw new InvalidVoteException("Invalid PROXY port");
}
}

private void parseV2(PushbackInputStream in, byte[] header, int length, Socket socket, long deadlineNanos,
ProxyHeaderResult result) throws Exception {
int versionCommand = header[12] & 0xFF;
int familyTransport = header[13] & 0xFF;
if ((versionCommand & 0xF0) != 0x20) {
throw new InvalidVoteException("Invalid PROXY protocol v2 version");
}
int command = versionCommand & 0x0F;
if (command == 0) {
discardFully(in, length, socket, deadlineNanos);
return;
}
if (command != 1) {
throw new InvalidVoteException("Unsupported PROXY protocol v2 command");
}
if (familyTransport == 0) {
discardFully(in, length, socket, deadlineNanos);
return;
}
int addressBytes;
int ipBytes;
if (familyTransport == 0x11) {
addressBytes = PROXY_V2_IPV4_BYTES;
ipBytes = 4;
} else if (familyTransport == 0x21) {
addressBytes = PROXY_V2_IPV6_BYTES;
ipBytes = 16;
} else {
throw new InvalidVoteException("Unsupported PROXY protocol v2 family or transport");
}
if (length < addressBytes) {
throw new InvalidVoteException("Incomplete PROXY protocol v2 address block");
}
byte[] addresses = new byte[addressBytes];
int read = 0;
while (read < addressBytes) {
int count = readWithDeadline(in, addresses, read, addressBytes - read, socket, deadlineNanos);
if (count == -1) {
throw new InvalidVoteException("Incomplete PROXY protocol v2 header");
}
read += count;
}
discardFully(in, length - addressBytes, socket, deadlineNanos);
result.setRealIp(IpLiteral.format(Arrays.copyOf(addresses, ipBytes)));
}

private int readPrefix(PushbackInputStream in, byte[] prefix, int targetLength, Socket socket, long deadlineNanos)
throws Exception {
return readPrefix(in, prefix, targetLength, 0, socket, deadlineNanos);
Expand Down Expand Up @@ -167,6 +279,11 @@ private void discardFully(PushbackInputStream in, int length, Socket socket, lon

private String readLine(PushbackInputStream in, Socket socket, long deadlineNanos, int maxLineBytes,
int[] totalBytes, String overflowMessage) throws Exception {
return readLine(in, socket, deadlineNanos, maxLineBytes, totalBytes, overflowMessage, false);
}

private String readLine(PushbackInputStream in, Socket socket, long deadlineNanos, int maxLineBytes,
int[] totalBytes, String overflowMessage, boolean requireCrLf) throws Exception {
ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream(Math.min(128, maxLineBytes));
int lineBytes = 0;

Expand All @@ -183,6 +300,9 @@ private String readLine(PushbackInputStream in, Socket socket, long deadlineNano
incrementTotalBytes(totalBytes);

if (value == '\n') {
if (requireCrLf) {
throw new InvalidVoteException("PROXY protocol v1 header requires CRLF");
}
break;
}

Expand All @@ -207,7 +327,7 @@ private String readLine(PushbackInputStream in, Socket socket, long deadlineNano
lineBuffer.write(value);
}

return lineBuffer.toString(StandardCharsets.US_ASCII.name()).trim();
return lineBuffer.toString(StandardCharsets.US_ASCII.name());
}

private void incrementTotalBytes(int[] totalBytes) throws InvalidVoteException {
Comment thread
BenCodez marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
Expand Down Expand Up @@ -314,6 +315,11 @@ public boolean isDisableV1() {

public abstract ThrottleConfig getThrottleConfig();

/** Socket peers allowed to supply PROXY protocol metadata. Empty by default. */
public Set<String> getTrustedProxyIps() {
return Collections.emptySet();
}

public abstract void logWarning(String warn);

public abstract void logSevere(String msg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,19 @@ public boolean isUseTokens() {
}

@Override
public java.util.Set<String> getTrustedProxyIps() {
Set<String> ips = new HashSet<String>();
ConfigurationNode node = getConfig().getNode("TrustedProxyIps");
if (node != null && !node.virtual()) {
for (ConfigurationNode item : node.childrenList()) {
Object raw = item.raw();
if (raw instanceof String) ips.add((String) raw);
}
}
return ips;
}

@Override
public ThrottleConfig getThrottleConfig() {
ConfigurationNode root = getConfig().getNode("ConnectionThrottle");
if (root == null || root.virtual()) {
Expand Down
Loading
Loading