From 5ff82ed97d4bfbb7a6ed95682ecb45cacb3ece28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Wed, 2 Oct 2024 16:00:31 +0200 Subject: [PATCH 01/53] added SOCKS5 support for DNS over tcp and udp (first working version) --- .../java/org/xbill/DNS/DefaultIoClient.java | 11 + src/main/java/org/xbill/DNS/NioTcpClient.java | 23 ++- .../java/org/xbill/DNS/SimpleResolver.java | 62 +++++- src/main/java/org/xbill/DNS/Socks5Proxy.java | 192 ++++++++++++++++++ .../java/org/xbill/DNS/io/TcpIoClient.java | 9 + 5 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/xbill/DNS/Socks5Proxy.java diff --git a/src/main/java/org/xbill/DNS/DefaultIoClient.java b/src/main/java/org/xbill/DNS/DefaultIoClient.java index 0f6cd407..6a980165 100644 --- a/src/main/java/org/xbill/DNS/DefaultIoClient.java +++ b/src/main/java/org/xbill/DNS/DefaultIoClient.java @@ -33,6 +33,17 @@ public CompletableFuture sendAndReceiveTcp( return tcpIoClient.sendAndReceiveTcp(local, remote, query, data, timeout); } + @Override + public CompletableFuture sendAndReceiveTcp( + InetSocketAddress local, + InetSocketAddress remote, + Socks5Proxy proxy, + Message query, + byte[] data, + Duration timeout) { + return tcpIoClient.sendAndReceiveTcp(local, remote, proxy, query, data, timeout); + } + @Override public CompletableFuture sendAndReceiveUdp( InetSocketAddress local, diff --git a/src/main/java/org/xbill/DNS/NioTcpClient.java b/src/main/java/org/xbill/DNS/NioTcpClient.java index 566c00eb..cebce858 100644 --- a/src/main/java/org/xbill/DNS/NioTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioTcpClient.java @@ -44,6 +44,9 @@ private void processPendingRegistrations() { if (!state.channel.isConnected()) { state.channel.register(selector, SelectionKey.OP_CONNECT, state); } else { + if (state.channel.keyFor(selector) == null) { + state.channel.register(selector, SelectionKey.OP_CONNECT, state); + } state.channel.keyFor(selector).interestOps(SelectionKey.OP_WRITE); } } catch (IOException e) { @@ -285,10 +288,21 @@ private static class ChannelKey { final InetSocketAddress remote; } + @Override + public CompletableFuture sendAndReceiveTcp( + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + Duration timeout) { + return this.sendAndReceiveTcp(local, remote, null, query, data, timeout); + } + @Override public CompletableFuture sendAndReceiveTcp( InetSocketAddress local, InetSocketAddress remote, + Socks5Proxy proxy, Message query, byte[] data, Duration timeout) { @@ -309,7 +323,14 @@ public CompletableFuture sendAndReceiveTcp( c.bind(local); } - c.connect(remote); + if (proxy != null) { + c.configureBlocking(true); + c.connect(proxy.getProxyAddress()); + proxy.socks5TcpHandshake(c, remote); + } else { + c.connect(remote); + } + c.configureBlocking(false); return new ChannelState(c); } catch (IOException e) { if (c != null) { diff --git a/src/main/java/org/xbill/DNS/SimpleResolver.java b/src/main/java/org/xbill/DNS/SimpleResolver.java index 4f5fd72a..4fb80935 100644 --- a/src/main/java/org/xbill/DNS/SimpleResolver.java +++ b/src/main/java/org/xbill/DNS/SimpleResolver.java @@ -7,6 +7,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.nio.channels.SocketChannel; import java.time.Duration; import java.util.List; import java.util.Objects; @@ -19,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.io.DefaultIoClientFactory; import org.xbill.DNS.io.IoClientFactory; +import org.xbill.DNS.io.TcpIoClient; /** * An implementation of Resolver that sends one query to one server. SimpleResolver handles TCP @@ -40,6 +42,7 @@ public class SimpleResolver implements Resolver { private InetSocketAddress address; private InetSocketAddress localAddress; + private Socks5Proxy proxy; private boolean useTCP; private boolean ignoreTruncation; private OPTRecord queryOPT = new OPTRecord(DEFAULT_EDNS_PAYLOADSIZE, 0, 0, 0); @@ -99,6 +102,11 @@ public SimpleResolver(InetSocketAddress host) { address = Objects.requireNonNull(host, "host must not be null"); } + /** Creates a SimpleResolver that will query the specified host via the specified SOCKS5 proxy */ + public SimpleResolver(Socks5Proxy socks5Proxy) { + proxy = Objects.requireNonNull(socks5Proxy, "proxy must not be null"); + } + /** Creates a SimpleResolver that will query the specified host */ public SimpleResolver(InetAddress host) { Objects.requireNonNull(host, "host must not be null"); @@ -387,22 +395,68 @@ CompletableFuture sendAsync(Message query, boolean forceTcp, Executor e } CompletableFuture result; + SocketChannel c = null; if (tcp) { - result = - ioClientFactory - .createOrGetTcpClient() - .sendAndReceiveTcp(localAddress, address, query, out, timeoutValue); + TcpIoClient tcpClient = ioClientFactory.createOrGetTcpClient(); + if (proxy != null) { + localAddress = proxy.getLocalAddress(); + address = proxy.getRemoteAddress(); + result = tcpClient.sendAndReceiveTcp(localAddress, address, proxy, query, out, timeoutValue); + } else { + result = tcpClient.sendAndReceiveTcp(localAddress, address, query, out, timeoutValue); + } } else { + if (proxy != null) { + try { + c = SocketChannel.open(); + if (localAddress != null) { + c.bind(localAddress); + } + if (proxy != null) { + c.connect(proxy.getProxyAddress()); + address = proxy.socks5UdpAssociateHandshake(c); + } + } catch (IOException e) { + if (c != null) { + try { + c.close(); + } catch (IOException ee) { + // ignore + } + } + return new CompletableFuture<>().thenComposeAsync(in -> { + CompletableFuture f = new CompletableFuture<>(); + f.completeExceptionally(new WireParseException("Error in Udp Associate SOCKS5 handshake", e)); + return f; + }, executor); + } + out = proxy.addUdpHeader(out, proxy.getRemoteAddress()); + localAddress = proxy.getLocalAddress(); + } result = ioClientFactory .createOrGetUdpClient() .sendAndReceiveUdp(localAddress, address, query, out, udpSize, timeoutValue); } + SocketChannel finalC = c; return result.thenComposeAsync( in -> { CompletableFuture f = new CompletableFuture<>(); + // finally close the tcp connection + // and remove SOCKS5 udp header from the response + if (proxy != null && !tcp) { + if (finalC != null) { + try { + finalC.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + in = proxy.removeUdpHeader(in); + } + // Check that the response is long enough. if (in.length < Header.LENGTH) { f.completeExceptionally(new WireParseException("invalid DNS header - too short")); diff --git a/src/main/java/org/xbill/DNS/Socks5Proxy.java b/src/main/java/org/xbill/DNS/Socks5Proxy.java new file mode 100644 index 00000000..66392ed8 --- /dev/null +++ b/src/main/java/org/xbill/DNS/Socks5Proxy.java @@ -0,0 +1,192 @@ +package org.xbill.DNS; + +import lombok.Getter; + +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +@Getter +public class Socks5Proxy { + private static final byte SOCKS5_VERSION = 0x05; + + private static final byte SOCKS5_AUTH_NONE = 0x00; + private static final byte SOCKS5_AUTH_GSSAPI = 0x01; + private static final byte SOCKS5_AUTH_USER_PASS = 0x02; + private static final byte SOCKS5_AUTH_NO_ACCEPTABLE_METHODS = (byte) 0xFF; + + private static final byte SOCKS5_CMD_CONNECT = 0x01; + private static final byte SOCKS5_CMD_BIND = 0x02; + private static final byte SOCKS5_CMD_UDP_ASSOCIATE = 0x03; + + private static final byte SOCKS5_ATYP_IPV4 = 0x01; + private static final byte SOCKS5_ATYP_DOMAINNAME = 0x03; + private static final byte SOCKS5_ATYP_IPV6 = 0x04; + + private static final byte SOCKS5_REP_SUCCEEDED = 0x00; + private static final byte SOCKS5_REP_GENERAL_FAILURE = 0x01; + private static final byte SOCKS5_REP_CONNECTION_NOT_ALLOWED = 0x02; + private static final byte SOCKS5_REP_NETWORK_UNREACHABLE = 0x03; + private static final byte SOCKS5_REP_HOST_UNREACHABLE = 0x04; + private static final byte SOCKS5_REP_CONNECTION_REFUSED = 0x05; + private static final byte SOCKS5_REP_TTL_EXPIRED = 0x06; + private static final byte SOCKS5_REP_COMMAND_NOT_SUPPORTED = 0x07; + private static final byte SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED = 0x08; + + private static final byte SOCKS5_RESERVED = 0x00; + + private final InetSocketAddress remoteAddress; + private final InetSocketAddress localAddress; + private final InetSocketAddress proxyAddress; + + + public Socks5Proxy(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress) { + this.remoteAddress = remoteAddress; + this.localAddress = localAddress; + this.proxyAddress = proxyAddress; + } + + public void socks5MethodSelection(SocketChannel c) { + ByteBuffer buffer = ByteBuffer.allocate(3); + buffer.put(SOCKS5_VERSION); + buffer.put((byte) 1); + buffer.put(SOCKS5_AUTH_NONE); + buffer.flip(); + + try { + c.write(buffer); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to write to TCP channel", e); + } + + buffer.clear(); + + try { + c.read(buffer); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to read from TCP channel", e); + } + + buffer.flip(); + if (buffer.get() != SOCKS5_VERSION) { + throw new IllegalArgumentException("Invalid version"); + } + + if (buffer.get() == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { + throw new IllegalArgumentException("No acceptable methods"); + } + } + + public void socks5HeaderExchange(SocketChannel c, InetSocketAddress remote) { + ByteBuffer buffer = ByteBuffer.allocate(10); + buffer.put(SOCKS5_VERSION); + buffer.put(SOCKS5_CMD_CONNECT); + buffer.put(SOCKS5_RESERVED); + buffer.put(SOCKS5_ATYP_IPV4); + buffer.put(remote.getAddress().getAddress()); + buffer.putShort((short) remote.getPort()); + buffer.flip(); + + try { + c.write(buffer); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to write to TCP channel", e); + } + buffer.clear(); + + try { + c.read(buffer); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to read from TCP channel", e); + } + buffer.flip(); + + if (buffer.get() != SOCKS5_VERSION) { + throw new IllegalArgumentException("Invalid version"); + } + + byte reply = buffer.get(); + if (reply != SOCKS5_REP_SUCCEEDED) { + throw new IllegalArgumentException("Failed to connect to remote server: " + reply); + } + } + + public InetSocketAddress socks5UdpAssociateExchange(SocketChannel c) { + ByteBuffer buffer = ByteBuffer.allocate(10); + buffer.put(SOCKS5_VERSION); + buffer.put(SOCKS5_CMD_UDP_ASSOCIATE); + buffer.put(SOCKS5_RESERVED); + buffer.put(SOCKS5_ATYP_IPV4); + buffer.put(new byte[] {0, 0, 0, 0}); + buffer.putShort((short) 0x00); + buffer.flip(); + + try { + c.write(buffer); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to write to TCP channel", e); + } + buffer.clear(); + + try { + c.read(buffer); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to read from TCP channel", e); + } + buffer.flip(); + + if (buffer.get() != SOCKS5_VERSION) { + throw new IllegalArgumentException("Invalid version"); + } + + byte reply = buffer.get(); + if (reply != SOCKS5_REP_SUCCEEDED) { + throw new IllegalArgumentException("Failed to connect to remote server: " + reply); + } + + buffer.get(); // skip RSV byte + + byte atyp = buffer.get(); + if (atyp != SOCKS5_ATYP_IPV4) { + throw new IllegalArgumentException("Invalid address type"); + } + + byte[] addr = new byte[4]; + buffer.get(addr); + int port = buffer.getShort() & 0xFFFF; + return new InetSocketAddress(this.getProxyAddress().getAddress(), port); + } + + public byte[] addUdpHeader(byte[] in, InetSocketAddress to) { + ByteBuffer buffer = ByteBuffer.allocate(in.length + 10); + buffer.put(SOCKS5_VERSION); + buffer.put(SOCKS5_RESERVED); + buffer.put(SOCKS5_RESERVED); + buffer.put(SOCKS5_ATYP_IPV4); + buffer.put(to.getAddress().getAddress()); + buffer.putShort((short) to.getPort()); + buffer.put(in); + + return buffer.array(); + } + + public byte[] removeUdpHeader(byte[] in) { + byte[] out = new byte[in.length - 10]; + System.arraycopy(in, 10, out, 0, in.length - 10); + return out; + } + + public void socks5TcpHandshake( + SocketChannel c, InetSocketAddress remote) { + this.socks5MethodSelection(c); + this.socks5HeaderExchange(c, remote); + } + + public InetSocketAddress socks5UdpAssociateHandshake( + SocketChannel c + ) throws UnknownHostException { + this.socks5MethodSelection(c); + return this.socks5UdpAssociateExchange(c); + } +} diff --git a/src/main/java/org/xbill/DNS/io/TcpIoClient.java b/src/main/java/org/xbill/DNS/io/TcpIoClient.java index e8febbef..b3f51aee 100644 --- a/src/main/java/org/xbill/DNS/io/TcpIoClient.java +++ b/src/main/java/org/xbill/DNS/io/TcpIoClient.java @@ -6,6 +6,7 @@ import java.util.concurrent.CompletableFuture; import org.xbill.DNS.Message; import org.xbill.DNS.Resolver; +import org.xbill.DNS.Socks5Proxy; /** * Serves as an interface from a {@link Resolver} to the underlying mechanism for sending bytes over @@ -32,4 +33,12 @@ CompletableFuture sendAndReceiveTcp( Message query, byte[] data, Duration timeout); + + CompletableFuture sendAndReceiveTcp( + InetSocketAddress local, + InetSocketAddress remote, + Socks5Proxy proxy, + Message query, + byte[] data, + Duration timeout); } From 5a4122593a6bcf6f16bf29417e90c8fc9fb85271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Thu, 3 Oct 2024 16:48:33 +0200 Subject: [PATCH 02/53] refactored code for udp associate into IoClient --- .../java/org/xbill/DNS/DefaultIoClient.java | 12 +++ src/main/java/org/xbill/DNS/NioUdpClient.java | 73 +++++++++++++++---- .../java/org/xbill/DNS/SimpleResolver.java | 48 ++---------- .../java/org/xbill/DNS/io/UdpIoClient.java | 10 +++ 4 files changed, 86 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/xbill/DNS/DefaultIoClient.java b/src/main/java/org/xbill/DNS/DefaultIoClient.java index 6a980165..f41f7d5f 100644 --- a/src/main/java/org/xbill/DNS/DefaultIoClient.java +++ b/src/main/java/org/xbill/DNS/DefaultIoClient.java @@ -54,4 +54,16 @@ public CompletableFuture sendAndReceiveUdp( Duration timeout) { return udpIoClient.sendAndReceiveUdp(local, remote, query, data, max, timeout); } + + @Override + public CompletableFuture sendAndReceiveUdp( + InetSocketAddress local, + InetSocketAddress remote, + Socks5Proxy proxy, + Message query, + byte[] data, + int max, + Duration timeout) { + return udpIoClient.sendAndReceiveUdp(local, remote, proxy, query, data, max, timeout); + } } diff --git a/src/main/java/org/xbill/DNS/NioUdpClient.java b/src/main/java/org/xbill/DNS/NioUdpClient.java index ce6607f2..091b861e 100644 --- a/src/main/java/org/xbill/DNS/NioUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioUdpClient.java @@ -10,6 +10,7 @@ import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; import java.security.SecureRandom; import java.time.Duration; import java.util.Iterator; @@ -63,7 +64,7 @@ private void processPendingRegistrations() { try { log.trace("Registering OP_READ for transaction with id {}", t.id); - t.channel.register(selector(), SelectionKey.OP_READ, t); + t.udpChannel.register(selector(), SelectionKey.OP_READ, t); t.send(); } catch (IOException e) { t.completeExceptionally(e); @@ -87,17 +88,19 @@ private class Transaction implements KeyProcessor { private final byte[] data; private final int max; private final long endTime; - private final DatagramChannel channel; + private final DatagramChannel udpChannel; + private final SocketChannel tcpChannel; + private final Socks5Proxy proxy; private final CompletableFuture f; void send() throws IOException { ByteBuffer buffer = ByteBuffer.wrap(data); verboseLog( "UDP write: transaction id=" + id, - channel.socket().getLocalSocketAddress(), - channel.socket().getRemoteSocketAddress(), + udpChannel.socket().getLocalSocketAddress(), + udpChannel.socket().getRemoteSocketAddress(), data); - int n = channel.send(buffer, channel.socket().getRemoteSocketAddress()); + int n = udpChannel.send(buffer, udpChannel.socket().getRemoteSocketAddress()); if (n == 0) { throw new EOFException( "Insufficient room for the datagram in the underlying output buffer for transaction " @@ -138,6 +141,14 @@ public void processReadyKey(SelectionKey key) { keyChannel.socket().getRemoteSocketAddress(), resultingData); silentDisconnectAndCloseChannel(); + if (proxy != null && tcpChannel != null) { + resultingData = proxy.removeUdpHeader(resultingData); + try { + tcpChannel.close(); + } catch (IOException e) { + // ignore, we either already have everything we need or can't do anything + } + } f.complete(resultingData); pendingTransactions.remove(this); } @@ -149,32 +160,64 @@ private void completeExceptionally(Exception e) { private void silentDisconnectAndCloseChannel() { try { - channel.disconnect(); + udpChannel.disconnect(); } catch (IOException e) { // ignore, we either already have everything we need or can't do anything } finally { - NioUdpClient.silentCloseChannel(channel); + NioUdpClient.silentCloseChannel(udpChannel); } } } + @Override + public CompletableFuture sendAndReceiveUdp( + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + int max, + Duration timeout) { + return sendAndReceiveUdp(local, remote, null, query, data, max, timeout); + } + @Override public CompletableFuture sendAndReceiveUdp( InetSocketAddress local, InetSocketAddress remote, + Socks5Proxy proxy, Message query, byte[] data, int max, Duration timeout) { long endTime = System.nanoTime() + timeout.toNanos(); CompletableFuture f = new CompletableFuture<>(); - DatagramChannel channel = null; + DatagramChannel udpChannel = null; + SocketChannel tcpChannel = null; try { final Selector selector = selector(); - channel = DatagramChannel.open(); - channel.configureBlocking(false); - Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, channel, f); + // SOCKS5 handshake to set up the UDP association + if (proxy != null) { + data = proxy.addUdpHeader(data, remote); + try { + tcpChannel = SocketChannel.open(); + if (local != null) { + tcpChannel.bind(local); + } + tcpChannel.connect(proxy.getProxyAddress()); + remote = proxy.socks5UdpAssociateHandshake(tcpChannel); + } catch (IOException e) { + return new CompletableFuture<>().thenComposeAsync(in -> { + f.completeExceptionally(new WireParseException("Error in Udp Associate SOCKS5 handshake", e)); + return f; + }); + } + } + + udpChannel = DatagramChannel.open(); + udpChannel.configureBlocking(false); + + Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, udpChannel, tcpChannel, proxy, f); if (local == null || local.getPort() == 0) { boolean bound = false; for (int i = 0; i < 1024; i++) { @@ -193,7 +236,7 @@ public CompletableFuture sendAndReceiveUdp( addr = new InetSocketAddress(local.getAddress(), port); } - channel.bind(addr); + udpChannel.bind(addr); bound = true; break; } catch (SocketException e) { @@ -207,16 +250,16 @@ public CompletableFuture sendAndReceiveUdp( } } - channel.connect(remote); + udpChannel.connect(remote); pendingTransactions.add(t); registrationQueue.add(t); selector.wakeup(); } catch (IOException e) { - silentCloseChannel(channel); + silentCloseChannel(udpChannel); f.completeExceptionally(e); } catch (Throwable e) { // Make sure to close the channel, no matter what, but only handle the declared IOException - silentCloseChannel(channel); + silentCloseChannel(udpChannel); throw e; } diff --git a/src/main/java/org/xbill/DNS/SimpleResolver.java b/src/main/java/org/xbill/DNS/SimpleResolver.java index 4fb80935..68793454 100644 --- a/src/main/java/org/xbill/DNS/SimpleResolver.java +++ b/src/main/java/org/xbill/DNS/SimpleResolver.java @@ -21,6 +21,7 @@ import org.xbill.DNS.io.DefaultIoClientFactory; import org.xbill.DNS.io.IoClientFactory; import org.xbill.DNS.io.TcpIoClient; +import org.xbill.DNS.io.UdpIoClient; /** * An implementation of Resolver that sends one query to one server. SimpleResolver handles TCP @@ -406,57 +407,20 @@ CompletableFuture sendAsync(Message query, boolean forceTcp, Executor e result = tcpClient.sendAndReceiveTcp(localAddress, address, query, out, timeoutValue); } } else { + UdpIoClient udpClient = ioClientFactory.createOrGetUdpClient(); if (proxy != null) { - try { - c = SocketChannel.open(); - if (localAddress != null) { - c.bind(localAddress); - } - if (proxy != null) { - c.connect(proxy.getProxyAddress()); - address = proxy.socks5UdpAssociateHandshake(c); - } - } catch (IOException e) { - if (c != null) { - try { - c.close(); - } catch (IOException ee) { - // ignore - } - } - return new CompletableFuture<>().thenComposeAsync(in -> { - CompletableFuture f = new CompletableFuture<>(); - f.completeExceptionally(new WireParseException("Error in Udp Associate SOCKS5 handshake", e)); - return f; - }, executor); - } - out = proxy.addUdpHeader(out, proxy.getRemoteAddress()); localAddress = proxy.getLocalAddress(); + address = proxy.getRemoteAddress(); + result = udpClient.sendAndReceiveUdp(localAddress, address, proxy, query, out, udpSize, timeoutValue); + } else { + result = udpClient.sendAndReceiveUdp(localAddress, address, query, out, udpSize, timeoutValue); } - result = - ioClientFactory - .createOrGetUdpClient() - .sendAndReceiveUdp(localAddress, address, query, out, udpSize, timeoutValue); } - SocketChannel finalC = c; return result.thenComposeAsync( in -> { CompletableFuture f = new CompletableFuture<>(); - // finally close the tcp connection - // and remove SOCKS5 udp header from the response - if (proxy != null && !tcp) { - if (finalC != null) { - try { - finalC.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - in = proxy.removeUdpHeader(in); - } - // Check that the response is long enough. if (in.length < Header.LENGTH) { f.completeExceptionally(new WireParseException("invalid DNS header - too short")); diff --git a/src/main/java/org/xbill/DNS/io/UdpIoClient.java b/src/main/java/org/xbill/DNS/io/UdpIoClient.java index 394ccc4b..175debd6 100644 --- a/src/main/java/org/xbill/DNS/io/UdpIoClient.java +++ b/src/main/java/org/xbill/DNS/io/UdpIoClient.java @@ -6,6 +6,7 @@ import java.util.concurrent.CompletableFuture; import org.xbill.DNS.Message; import org.xbill.DNS.Resolver; +import org.xbill.DNS.Socks5Proxy; /** * Serves as an interface from a {@link Resolver} to the underlying mechanism for sending bytes over @@ -34,4 +35,13 @@ CompletableFuture sendAndReceiveUdp( byte[] data, int max, Duration timeout); + + CompletableFuture sendAndReceiveUdp( + InetSocketAddress local, + InetSocketAddress remote, + Socks5Proxy proxy, + Message query, + byte[] data, + int max, + Duration timeout); } From bee0c76a99a8f7663a2b713b3faa05a66828b545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Fri, 18 Oct 2024 11:57:45 +0200 Subject: [PATCH 03/53] added user/pwd authentication and refactored some of the code --- src/main/java/org/xbill/DNS/Socks5Proxy.java | 147 ++++++++++++------- 1 file changed, 95 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/xbill/DNS/Socks5Proxy.java b/src/main/java/org/xbill/DNS/Socks5Proxy.java index 66392ed8..637b08ba 100644 --- a/src/main/java/org/xbill/DNS/Socks5Proxy.java +++ b/src/main/java/org/xbill/DNS/Socks5Proxy.java @@ -1,16 +1,16 @@ package org.xbill.DNS; import lombok.Getter; - import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; +import java.util.Objects; @Getter public class Socks5Proxy { private static final byte SOCKS5_VERSION = 0x05; - + private static final byte SOCKS5_USER_PWD_AUTH_VERSION = 0x01; private static final byte SOCKS5_AUTH_NONE = 0x00; private static final byte SOCKS5_AUTH_GSSAPI = 0x01; private static final byte SOCKS5_AUTH_USER_PASS = 0x02; @@ -39,42 +39,87 @@ public class Socks5Proxy { private final InetSocketAddress remoteAddress; private final InetSocketAddress localAddress; private final InetSocketAddress proxyAddress; - + private final String socks5User; + private final String socks5Password; + + public Socks5Proxy(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress, String socks5User, String socks5Password) { + this.remoteAddress = Objects.requireNonNull(remoteAddress, "remoteAddress must not be null"); + this.localAddress = Objects.requireNonNull(localAddress, "localAddress must not be null"); + this.proxyAddress = Objects.requireNonNull(proxyAddress, "proxyAddress must not be null"); + this.socks5User = socks5User; + this.socks5Password = socks5Password; + } public Socks5Proxy(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress) { - this.remoteAddress = remoteAddress; - this.localAddress = localAddress; - this.proxyAddress = proxyAddress; + this(proxyAddress, remoteAddress, localAddress, null, null); } - public void socks5MethodSelection(SocketChannel c) { - ByteBuffer buffer = ByteBuffer.allocate(3); - buffer.put(SOCKS5_VERSION); - buffer.put((byte) 1); - buffer.put(SOCKS5_AUTH_NONE); - buffer.flip(); - + private void writeToChannel(SocketChannel c, ByteBuffer buffer) { try { c.write(buffer); } catch (Exception e) { - throw new IllegalArgumentException("Failed to write to TCP channel", e); + throw new IllegalStateException("Failed to write to TCP channel", e); } + } - buffer.clear(); - + private void readFromChannel(SocketChannel c, ByteBuffer buffer) { try { c.read(buffer); } catch (Exception e) { - throw new IllegalArgumentException("Failed to read from TCP channel", e); + throw new IllegalStateException("Failed to read from TCP channel", e); } + } + public byte socks5MethodSelection(SocketChannel c) { + ByteBuffer buffer = ByteBuffer.allocate(3); + buffer.put(SOCKS5_VERSION); + buffer.put((byte) 1); + buffer.put((this.socks5User != null && this.socks5Password != null) ? SOCKS5_AUTH_USER_PASS : SOCKS5_AUTH_NONE); buffer.flip(); + + writeToChannel(c, buffer); + buffer.clear(); + + readFromChannel(c, buffer); + buffer.flip(); + if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalArgumentException("Invalid version"); + throw new IllegalStateException("Invalid SOCKS5 version"); + } + + byte method = buffer.get(); + if (method == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { + throw new IllegalStateException("No acceptable authentication methods"); + } + return method; + } + + public void socks5UserPwdAuthExchange(SocketChannel c) { + ByteBuffer buffer = ByteBuffer.allocate(520); + buffer.put(SOCKS5_USER_PWD_AUTH_VERSION); + buffer.put((byte) this.socks5User.length()); + buffer.put(this.socks5User.getBytes()); + buffer.put((byte) this.socks5Password.length()); + buffer.put(this.socks5Password.getBytes()); + buffer.flip(); + + writeToChannel(c, buffer); + buffer.clear(); + + readFromChannel(c, buffer); + buffer.flip(); + + if (!buffer.hasRemaining()) { + throw new IllegalStateException("Authentication failed. No data received from server"); + } + + if (buffer.get() != SOCKS5_USER_PWD_AUTH_VERSION) { + throw new IllegalStateException("Invalid user/pwd auth subnegotiation version"); } - if (buffer.get() == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { - throw new IllegalArgumentException("No acceptable methods"); + byte reply = buffer.get(); + if (reply != SOCKS5_REP_SUCCEEDED) { + throw new IllegalStateException("Authentication failed with status " + reply); } } @@ -88,27 +133,23 @@ public void socks5HeaderExchange(SocketChannel c, InetSocketAddress remote) { buffer.putShort((short) remote.getPort()); buffer.flip(); - try { - c.write(buffer); - } catch (Exception e) { - throw new IllegalArgumentException("Failed to write to TCP channel", e); - } + writeToChannel(c, buffer); buffer.clear(); - try { - c.read(buffer); - } catch (Exception e) { - throw new IllegalArgumentException("Failed to read from TCP channel", e); - } + readFromChannel(c, buffer); buffer.flip(); + if (!buffer.hasRemaining()) { + throw new IllegalStateException("SOCKS5 handshake failed. No data received from server"); + } + if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalArgumentException("Invalid version"); + throw new IllegalStateException("Invalid SOCKS5 version"); } byte reply = buffer.get(); if (reply != SOCKS5_REP_SUCCEEDED) { - throw new IllegalArgumentException("Failed to connect to remote server: " + reply); + throw new IllegalStateException("Connection to remote server failed: " + reply); } } @@ -122,34 +163,30 @@ public InetSocketAddress socks5UdpAssociateExchange(SocketChannel c) { buffer.putShort((short) 0x00); buffer.flip(); - try { - c.write(buffer); - } catch (Exception e) { - throw new IllegalArgumentException("Failed to write to TCP channel", e); - } + writeToChannel(c, buffer); buffer.clear(); - try { - c.read(buffer); - } catch (Exception e) { - throw new IllegalArgumentException("Failed to read from TCP channel", e); - } + readFromChannel(c, buffer); buffer.flip(); + if (!buffer.hasRemaining()) { + throw new IllegalStateException("SOCKS5 udp associate exchange failed. No data received from server"); + } + if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalArgumentException("Invalid version"); + throw new IllegalStateException("Invalid SOCKS5 version"); } byte reply = buffer.get(); if (reply != SOCKS5_REP_SUCCEEDED) { - throw new IllegalArgumentException("Failed to connect to remote server: " + reply); + throw new IllegalStateException("UDP association failed: " + reply); } buffer.get(); // skip RSV byte byte atyp = buffer.get(); if (atyp != SOCKS5_ATYP_IPV4) { - throw new IllegalArgumentException("Invalid address type"); + throw new IllegalStateException("Invalid address type"); } byte[] addr = new byte[4]; @@ -177,16 +214,22 @@ public byte[] removeUdpHeader(byte[] in) { return out; } - public void socks5TcpHandshake( - SocketChannel c, InetSocketAddress remote) { - this.socks5MethodSelection(c); + public void socks5TcpHandshake(SocketChannel c, InetSocketAddress remote) { + byte method = this.socks5MethodSelection(c); + if (method == SOCKS5_AUTH_USER_PASS) { + this.socks5UserPwdAuthExchange(c); + } this.socks5HeaderExchange(c, remote); } - public InetSocketAddress socks5UdpAssociateHandshake( - SocketChannel c - ) throws UnknownHostException { - this.socks5MethodSelection(c); + public InetSocketAddress socks5UdpAssociateHandshake(SocketChannel c) throws UnknownHostException { + byte method = this.socks5MethodSelection(c); + if (method == SOCKS5_AUTH_USER_PASS) { + if (this.socks5User == null || this.socks5Password == null) { + throw new IllegalStateException("No user or password provided"); + } + this.socks5UserPwdAuthExchange(c); + } return this.socks5UdpAssociateExchange(c); } } From 2a1192c822c90d3c7a4809a6daf766f2b952317d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Thu, 5 Dec 2024 18:16:31 +0100 Subject: [PATCH 04/53] redo code changes in SimpleResolver and the IoClients --- .../java/org/xbill/DNS/DefaultIoClient.java | 23 ------ src/main/java/org/xbill/DNS/NioTcpClient.java | 23 +----- src/main/java/org/xbill/DNS/NioUdpClient.java | 73 ++++--------------- .../java/org/xbill/DNS/SimpleResolver.java | 34 ++------- .../java/org/xbill/DNS/io/TcpIoClient.java | 9 --- .../java/org/xbill/DNS/io/UdpIoClient.java | 10 --- 6 files changed, 24 insertions(+), 148 deletions(-) diff --git a/src/main/java/org/xbill/DNS/DefaultIoClient.java b/src/main/java/org/xbill/DNS/DefaultIoClient.java index f41f7d5f..0f6cd407 100644 --- a/src/main/java/org/xbill/DNS/DefaultIoClient.java +++ b/src/main/java/org/xbill/DNS/DefaultIoClient.java @@ -33,17 +33,6 @@ public CompletableFuture sendAndReceiveTcp( return tcpIoClient.sendAndReceiveTcp(local, remote, query, data, timeout); } - @Override - public CompletableFuture sendAndReceiveTcp( - InetSocketAddress local, - InetSocketAddress remote, - Socks5Proxy proxy, - Message query, - byte[] data, - Duration timeout) { - return tcpIoClient.sendAndReceiveTcp(local, remote, proxy, query, data, timeout); - } - @Override public CompletableFuture sendAndReceiveUdp( InetSocketAddress local, @@ -54,16 +43,4 @@ public CompletableFuture sendAndReceiveUdp( Duration timeout) { return udpIoClient.sendAndReceiveUdp(local, remote, query, data, max, timeout); } - - @Override - public CompletableFuture sendAndReceiveUdp( - InetSocketAddress local, - InetSocketAddress remote, - Socks5Proxy proxy, - Message query, - byte[] data, - int max, - Duration timeout) { - return udpIoClient.sendAndReceiveUdp(local, remote, proxy, query, data, max, timeout); - } } diff --git a/src/main/java/org/xbill/DNS/NioTcpClient.java b/src/main/java/org/xbill/DNS/NioTcpClient.java index cebce858..566c00eb 100644 --- a/src/main/java/org/xbill/DNS/NioTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioTcpClient.java @@ -44,9 +44,6 @@ private void processPendingRegistrations() { if (!state.channel.isConnected()) { state.channel.register(selector, SelectionKey.OP_CONNECT, state); } else { - if (state.channel.keyFor(selector) == null) { - state.channel.register(selector, SelectionKey.OP_CONNECT, state); - } state.channel.keyFor(selector).interestOps(SelectionKey.OP_WRITE); } } catch (IOException e) { @@ -288,21 +285,10 @@ private static class ChannelKey { final InetSocketAddress remote; } - @Override - public CompletableFuture sendAndReceiveTcp( - InetSocketAddress local, - InetSocketAddress remote, - Message query, - byte[] data, - Duration timeout) { - return this.sendAndReceiveTcp(local, remote, null, query, data, timeout); - } - @Override public CompletableFuture sendAndReceiveTcp( InetSocketAddress local, InetSocketAddress remote, - Socks5Proxy proxy, Message query, byte[] data, Duration timeout) { @@ -323,14 +309,7 @@ public CompletableFuture sendAndReceiveTcp( c.bind(local); } - if (proxy != null) { - c.configureBlocking(true); - c.connect(proxy.getProxyAddress()); - proxy.socks5TcpHandshake(c, remote); - } else { - c.connect(remote); - } - c.configureBlocking(false); + c.connect(remote); return new ChannelState(c); } catch (IOException e) { if (c != null) { diff --git a/src/main/java/org/xbill/DNS/NioUdpClient.java b/src/main/java/org/xbill/DNS/NioUdpClient.java index 091b861e..ce6607f2 100644 --- a/src/main/java/org/xbill/DNS/NioUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioUdpClient.java @@ -10,7 +10,6 @@ import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; import java.security.SecureRandom; import java.time.Duration; import java.util.Iterator; @@ -64,7 +63,7 @@ private void processPendingRegistrations() { try { log.trace("Registering OP_READ for transaction with id {}", t.id); - t.udpChannel.register(selector(), SelectionKey.OP_READ, t); + t.channel.register(selector(), SelectionKey.OP_READ, t); t.send(); } catch (IOException e) { t.completeExceptionally(e); @@ -88,19 +87,17 @@ private class Transaction implements KeyProcessor { private final byte[] data; private final int max; private final long endTime; - private final DatagramChannel udpChannel; - private final SocketChannel tcpChannel; - private final Socks5Proxy proxy; + private final DatagramChannel channel; private final CompletableFuture f; void send() throws IOException { ByteBuffer buffer = ByteBuffer.wrap(data); verboseLog( "UDP write: transaction id=" + id, - udpChannel.socket().getLocalSocketAddress(), - udpChannel.socket().getRemoteSocketAddress(), + channel.socket().getLocalSocketAddress(), + channel.socket().getRemoteSocketAddress(), data); - int n = udpChannel.send(buffer, udpChannel.socket().getRemoteSocketAddress()); + int n = channel.send(buffer, channel.socket().getRemoteSocketAddress()); if (n == 0) { throw new EOFException( "Insufficient room for the datagram in the underlying output buffer for transaction " @@ -141,14 +138,6 @@ public void processReadyKey(SelectionKey key) { keyChannel.socket().getRemoteSocketAddress(), resultingData); silentDisconnectAndCloseChannel(); - if (proxy != null && tcpChannel != null) { - resultingData = proxy.removeUdpHeader(resultingData); - try { - tcpChannel.close(); - } catch (IOException e) { - // ignore, we either already have everything we need or can't do anything - } - } f.complete(resultingData); pendingTransactions.remove(this); } @@ -160,64 +149,32 @@ private void completeExceptionally(Exception e) { private void silentDisconnectAndCloseChannel() { try { - udpChannel.disconnect(); + channel.disconnect(); } catch (IOException e) { // ignore, we either already have everything we need or can't do anything } finally { - NioUdpClient.silentCloseChannel(udpChannel); + NioUdpClient.silentCloseChannel(channel); } } } - @Override - public CompletableFuture sendAndReceiveUdp( - InetSocketAddress local, - InetSocketAddress remote, - Message query, - byte[] data, - int max, - Duration timeout) { - return sendAndReceiveUdp(local, remote, null, query, data, max, timeout); - } - @Override public CompletableFuture sendAndReceiveUdp( InetSocketAddress local, InetSocketAddress remote, - Socks5Proxy proxy, Message query, byte[] data, int max, Duration timeout) { long endTime = System.nanoTime() + timeout.toNanos(); CompletableFuture f = new CompletableFuture<>(); - DatagramChannel udpChannel = null; - SocketChannel tcpChannel = null; + DatagramChannel channel = null; try { final Selector selector = selector(); + channel = DatagramChannel.open(); + channel.configureBlocking(false); - // SOCKS5 handshake to set up the UDP association - if (proxy != null) { - data = proxy.addUdpHeader(data, remote); - try { - tcpChannel = SocketChannel.open(); - if (local != null) { - tcpChannel.bind(local); - } - tcpChannel.connect(proxy.getProxyAddress()); - remote = proxy.socks5UdpAssociateHandshake(tcpChannel); - } catch (IOException e) { - return new CompletableFuture<>().thenComposeAsync(in -> { - f.completeExceptionally(new WireParseException("Error in Udp Associate SOCKS5 handshake", e)); - return f; - }); - } - } - - udpChannel = DatagramChannel.open(); - udpChannel.configureBlocking(false); - - Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, udpChannel, tcpChannel, proxy, f); + Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, channel, f); if (local == null || local.getPort() == 0) { boolean bound = false; for (int i = 0; i < 1024; i++) { @@ -236,7 +193,7 @@ public CompletableFuture sendAndReceiveUdp( addr = new InetSocketAddress(local.getAddress(), port); } - udpChannel.bind(addr); + channel.bind(addr); bound = true; break; } catch (SocketException e) { @@ -250,16 +207,16 @@ public CompletableFuture sendAndReceiveUdp( } } - udpChannel.connect(remote); + channel.connect(remote); pendingTransactions.add(t); registrationQueue.add(t); selector.wakeup(); } catch (IOException e) { - silentCloseChannel(udpChannel); + silentCloseChannel(channel); f.completeExceptionally(e); } catch (Throwable e) { // Make sure to close the channel, no matter what, but only handle the declared IOException - silentCloseChannel(udpChannel); + silentCloseChannel(channel); throw e; } diff --git a/src/main/java/org/xbill/DNS/SimpleResolver.java b/src/main/java/org/xbill/DNS/SimpleResolver.java index 68793454..dc919830 100644 --- a/src/main/java/org/xbill/DNS/SimpleResolver.java +++ b/src/main/java/org/xbill/DNS/SimpleResolver.java @@ -7,7 +7,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; -import java.nio.channels.SocketChannel; import java.time.Duration; import java.util.List; import java.util.Objects; @@ -20,8 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.io.DefaultIoClientFactory; import org.xbill.DNS.io.IoClientFactory; -import org.xbill.DNS.io.TcpIoClient; -import org.xbill.DNS.io.UdpIoClient; /** * An implementation of Resolver that sends one query to one server. SimpleResolver handles TCP @@ -43,7 +40,6 @@ public class SimpleResolver implements Resolver { private InetSocketAddress address; private InetSocketAddress localAddress; - private Socks5Proxy proxy; private boolean useTCP; private boolean ignoreTruncation; private OPTRecord queryOPT = new OPTRecord(DEFAULT_EDNS_PAYLOADSIZE, 0, 0, 0); @@ -103,11 +99,6 @@ public SimpleResolver(InetSocketAddress host) { address = Objects.requireNonNull(host, "host must not be null"); } - /** Creates a SimpleResolver that will query the specified host via the specified SOCKS5 proxy */ - public SimpleResolver(Socks5Proxy socks5Proxy) { - proxy = Objects.requireNonNull(socks5Proxy, "proxy must not be null"); - } - /** Creates a SimpleResolver that will query the specified host */ public SimpleResolver(InetAddress host) { Objects.requireNonNull(host, "host must not be null"); @@ -396,25 +387,16 @@ CompletableFuture sendAsync(Message query, boolean forceTcp, Executor e } CompletableFuture result; - SocketChannel c = null; if (tcp) { - TcpIoClient tcpClient = ioClientFactory.createOrGetTcpClient(); - if (proxy != null) { - localAddress = proxy.getLocalAddress(); - address = proxy.getRemoteAddress(); - result = tcpClient.sendAndReceiveTcp(localAddress, address, proxy, query, out, timeoutValue); - } else { - result = tcpClient.sendAndReceiveTcp(localAddress, address, query, out, timeoutValue); - } + result = + ioClientFactory + .createOrGetTcpClient() + .sendAndReceiveTcp(localAddress, address, query, out, timeoutValue); } else { - UdpIoClient udpClient = ioClientFactory.createOrGetUdpClient(); - if (proxy != null) { - localAddress = proxy.getLocalAddress(); - address = proxy.getRemoteAddress(); - result = udpClient.sendAndReceiveUdp(localAddress, address, proxy, query, out, udpSize, timeoutValue); - } else { - result = udpClient.sendAndReceiveUdp(localAddress, address, query, out, udpSize, timeoutValue); - } + result = + ioClientFactory + .createOrGetUdpClient() + .sendAndReceiveUdp(localAddress, address, query, out, udpSize, timeoutValue); } return result.thenComposeAsync( diff --git a/src/main/java/org/xbill/DNS/io/TcpIoClient.java b/src/main/java/org/xbill/DNS/io/TcpIoClient.java index b3f51aee..e8febbef 100644 --- a/src/main/java/org/xbill/DNS/io/TcpIoClient.java +++ b/src/main/java/org/xbill/DNS/io/TcpIoClient.java @@ -6,7 +6,6 @@ import java.util.concurrent.CompletableFuture; import org.xbill.DNS.Message; import org.xbill.DNS.Resolver; -import org.xbill.DNS.Socks5Proxy; /** * Serves as an interface from a {@link Resolver} to the underlying mechanism for sending bytes over @@ -33,12 +32,4 @@ CompletableFuture sendAndReceiveTcp( Message query, byte[] data, Duration timeout); - - CompletableFuture sendAndReceiveTcp( - InetSocketAddress local, - InetSocketAddress remote, - Socks5Proxy proxy, - Message query, - byte[] data, - Duration timeout); } diff --git a/src/main/java/org/xbill/DNS/io/UdpIoClient.java b/src/main/java/org/xbill/DNS/io/UdpIoClient.java index 175debd6..394ccc4b 100644 --- a/src/main/java/org/xbill/DNS/io/UdpIoClient.java +++ b/src/main/java/org/xbill/DNS/io/UdpIoClient.java @@ -6,7 +6,6 @@ import java.util.concurrent.CompletableFuture; import org.xbill.DNS.Message; import org.xbill.DNS.Resolver; -import org.xbill.DNS.Socks5Proxy; /** * Serves as an interface from a {@link Resolver} to the underlying mechanism for sending bytes over @@ -35,13 +34,4 @@ CompletableFuture sendAndReceiveUdp( byte[] data, int max, Duration timeout); - - CompletableFuture sendAndReceiveUdp( - InetSocketAddress local, - InetSocketAddress remote, - Socks5Proxy proxy, - Message query, - byte[] data, - int max, - Duration timeout); } From 80dfcb629529cc578d3227b9d2ecff181af499ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Thu, 5 Dec 2024 18:21:03 +0100 Subject: [PATCH 05/53] implemented SOCKS5 requests in an async way and added requested changes --- src/main/java/org/xbill/DNS/Socks5Proxy.java | 490 ++++++++++++------ .../java/org/xbill/DNS/Socks5ProxyConfig.java | 37 ++ .../xbill/DNS/Socks5ProxyIoClientFactory.java | 236 +++++++++ .../org/xbill/DNS/Socks5ProxyTcpIoClient.java | 156 ++++++ .../org/xbill/DNS/Socks5ProxyUdpIoClient.java | 158 ++++++ 5 files changed, 929 insertions(+), 148 deletions(-) create mode 100644 src/main/java/org/xbill/DNS/Socks5ProxyConfig.java create mode 100644 src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java create mode 100644 src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java create mode 100644 src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java diff --git a/src/main/java/org/xbill/DNS/Socks5Proxy.java b/src/main/java/org/xbill/DNS/Socks5Proxy.java index 637b08ba..fe7ab9fe 100644 --- a/src/main/java/org/xbill/DNS/Socks5Proxy.java +++ b/src/main/java/org/xbill/DNS/Socks5Proxy.java @@ -1,13 +1,23 @@ package org.xbill.DNS; import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetSocketAddress; -import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; -import java.util.Objects; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +@Slf4j @Getter +@Setter public class Socks5Proxy { private static final byte SOCKS5_VERSION = 0x05; private static final byte SOCKS5_USER_PWD_AUTH_VERSION = 0x01; @@ -36,200 +46,384 @@ public class Socks5Proxy { private static final byte SOCKS5_RESERVED = 0x00; - private final InetSocketAddress remoteAddress; - private final InetSocketAddress localAddress; - private final InetSocketAddress proxyAddress; - private final String socks5User; - private final String socks5Password; - - public Socks5Proxy(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress, String socks5User, String socks5Password) { - this.remoteAddress = Objects.requireNonNull(remoteAddress, "remoteAddress must not be null"); - this.localAddress = Objects.requireNonNull(localAddress, "localAddress must not be null"); - this.proxyAddress = Objects.requireNonNull(proxyAddress, "proxyAddress must not be null"); - this.socks5User = socks5User; - this.socks5Password = socks5Password; + public enum State { + INIT, + UDP_ASSOCIATE, + CONNECTED, + FAILED } - public Socks5Proxy(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress) { - this(proxyAddress, remoteAddress, localAddress, null, null); + public enum Command { + CONNECT, + UDP_ASSOCIATE } - private void writeToChannel(SocketChannel c, ByteBuffer buffer) { - try { - c.write(buffer); - } catch (Exception e) { - throw new IllegalStateException("Failed to write to TCP channel", e); - } + private final Socks5ProxyConfig config; + private final InetSocketAddress local; + private final InetSocketAddress remote; + private SelectionKey tcpSelectionKey; + private DatagramChannel udpChannel; + private final Command command; + private State state = State.INIT; + + public Socks5Proxy( + SelectionKey tcpSelectionKey, + Socks5ProxyConfig config, + InetSocketAddress local, + InetSocketAddress remote, + Command command) { + this.tcpSelectionKey = tcpSelectionKey; + this.config = config; + this.local = local; + this.remote = remote; + this.command = command; } - private void readFromChannel(SocketChannel c, ByteBuffer buffer) { - try { - c.read(buffer); - } catch (Exception e) { - throw new IllegalStateException("Failed to read from TCP channel", e); - } + public void handleSOCKS5(CompletableFuture future) { + tcpSelectionKey.attach(new ConnectHandler(future)); + tcpSelectionKey.interestOps(SelectionKey.OP_CONNECT); + tcpSelectionKey.selector().wakeup(); } - public byte socks5MethodSelection(SocketChannel c) { - ByteBuffer buffer = ByteBuffer.allocate(3); - buffer.put(SOCKS5_VERSION); - buffer.put((byte) 1); - buffer.put((this.socks5User != null && this.socks5Password != null) ? SOCKS5_AUTH_USER_PASS : SOCKS5_AUTH_NONE); - buffer.flip(); - - writeToChannel(c, buffer); - buffer.clear(); - - readFromChannel(c, buffer); - buffer.flip(); - - if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalStateException("Invalid SOCKS5 version"); + public ByteBuffer addSocks5UdpAssociateHeader(byte[] data) { + ByteBuffer buffer; + byte addressType; + byte[] addressBytes; + if (remote.getAddress() instanceof Inet4Address) { + addressType = SOCKS5_ATYP_IPV4; + addressBytes = remote.getAddress().getAddress(); + buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); + } else if (remote.getAddress() instanceof Inet6Address) { + addressType = SOCKS5_ATYP_IPV6; + addressBytes = remote.getAddress().getAddress(); + buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); + } else { + addressType = SOCKS5_ATYP_DOMAINNAME; + addressBytes = remote.getHostName().getBytes(StandardCharsets.UTF_8); + buffer = ByteBuffer.allocate(4 + 1 + addressBytes.length + 2 + data.length); } - byte method = buffer.get(); - if (method == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { - throw new IllegalStateException("No acceptable authentication methods"); + buffer.put((byte) 0x00); // RSV + buffer.put((byte) 0x00); // RSV + buffer.put((byte) 0x00); // FRAG + buffer.put(addressType); // ATYP (IPv4) + if (addressType == SOCKS5_ATYP_DOMAINNAME) { + buffer.put((byte) addressBytes.length); } - return method; - } + buffer.put(addressBytes); // DST.ADDR + buffer.putShort((short) remote.getPort()); // DST.PORT + buffer.put(data); // DATA - public void socks5UserPwdAuthExchange(SocketChannel c) { - ByteBuffer buffer = ByteBuffer.allocate(520); - buffer.put(SOCKS5_USER_PWD_AUTH_VERSION); - buffer.put((byte) this.socks5User.length()); - buffer.put(this.socks5User.getBytes()); - buffer.put((byte) this.socks5Password.length()); - buffer.put(this.socks5Password.getBytes()); - buffer.flip(); + return buffer; + } - writeToChannel(c, buffer); - buffer.clear(); + private class ConnectHandler implements Runnable { + private final CompletableFuture future; - readFromChannel(c, buffer); - buffer.flip(); + ConnectHandler(CompletableFuture future) { + this.future = future; + } - if (!buffer.hasRemaining()) { - throw new IllegalStateException("Authentication failed. No data received from server"); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + if (channel.finishConnect()) { + // Connection finished successfully + tcpSelectionKey.attach(new Socks5MethodSelectionHandler(future)); + tcpSelectionKey.interestOps(SelectionKey.OP_WRITE); + } else { + // Connection not finished, re-register for OP_CONNECT + tcpSelectionKey.interestOps(SelectionKey.OP_CONNECT); + } + tcpSelectionKey.selector().wakeup(); + } catch (IOException e) { + future.completeExceptionally(e); + } } + } + + private class Socks5MethodSelectionHandler implements Runnable { + private final CompletableFuture future; - if (buffer.get() != SOCKS5_USER_PWD_AUTH_VERSION) { - throw new IllegalStateException("Invalid user/pwd auth subnegotiation version"); + Socks5MethodSelectionHandler(CompletableFuture future) { + this.future = future; } - byte reply = buffer.get(); - if (reply != SOCKS5_REP_SUCCEEDED) { - throw new IllegalStateException("Authentication failed with status " + reply); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + ByteBuffer buffer = ByteBuffer.allocate(3); + buffer.put(SOCKS5_VERSION); + buffer.put((byte) 1); + buffer.put((config.getAuthMethod() == Socks5ProxyConfig.AuthMethod.USER_PASS) ? SOCKS5_AUTH_USER_PASS : SOCKS5_AUTH_NONE); + buffer.flip(); + channel.write(buffer); + + tcpSelectionKey.attach(new Socks5MethodSelectionReadHandler(future)); + tcpSelectionKey.interestOps(SelectionKey.OP_READ); + tcpSelectionKey.selector().wakeup(); + } catch (IOException e) { + future.completeExceptionally(e); + } } } - public void socks5HeaderExchange(SocketChannel c, InetSocketAddress remote) { - ByteBuffer buffer = ByteBuffer.allocate(10); - buffer.put(SOCKS5_VERSION); - buffer.put(SOCKS5_CMD_CONNECT); - buffer.put(SOCKS5_RESERVED); - buffer.put(SOCKS5_ATYP_IPV4); - buffer.put(remote.getAddress().getAddress()); - buffer.putShort((short) remote.getPort()); - buffer.flip(); - - writeToChannel(c, buffer); - buffer.clear(); + private class Socks5MethodSelectionReadHandler implements Runnable { + private final CompletableFuture future; - readFromChannel(c, buffer); - buffer.flip(); + Socks5MethodSelectionReadHandler(CompletableFuture future) { + this.future = future; + } - if (!buffer.hasRemaining()) { - throw new IllegalStateException("SOCKS5 handshake failed. No data received from server"); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + ByteBuffer buffer = ByteBuffer.allocate(2); + channel.read(buffer); + buffer.flip(); + + if (buffer.get() != SOCKS5_VERSION) { + throw new IllegalStateException("Invalid SOCKS5 version"); + } + + byte method = buffer.get(); + if (method == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { + throw new IllegalStateException("No acceptable authentication methods"); + } + + if (method == SOCKS5_AUTH_USER_PASS) { + tcpSelectionKey.attach(new Socks5UserPassAuthHandler(future)); + } else { + if (command == Command.CONNECT) { + tcpSelectionKey.attach(new Socks5ConnectExchangeHandler(future)); + } else if (command == Command.UDP_ASSOCIATE) { + tcpSelectionKey.attach(new Socks5UdpAssociateExchangeHandler(future)); + } else { + throw new IllegalStateException("Unsupported command: " + command); + } + } + + tcpSelectionKey.interestOps(SelectionKey.OP_WRITE); + tcpSelectionKey.selector().wakeup(); + } catch (IOException | IllegalStateException e) { + future.completeExceptionally(e); + } } + } + + private class Socks5UserPassAuthHandler implements Runnable { + private final CompletableFuture future; - if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalStateException("Invalid SOCKS5 version"); + Socks5UserPassAuthHandler(CompletableFuture future) { + this.future = future; } - byte reply = buffer.get(); - if (reply != SOCKS5_REP_SUCCEEDED) { - throw new IllegalStateException("Connection to remote server failed: " + reply); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + ByteBuffer buffer = ByteBuffer.allocate(2 + config.getSocks5User().length() + 2 + config.getSocks5Password().length()); + buffer.put(SOCKS5_USER_PWD_AUTH_VERSION); + buffer.put((byte) config.getSocks5User().length()); + buffer.put(config.getSocks5User().getBytes()); + buffer.put((byte) config.getSocks5Password().length()); + buffer.put(config.getSocks5Password().getBytes()); + buffer.flip(); + channel.write(buffer); + + tcpSelectionKey.attach(new Socks5UserPassAuthReadHandler(future)); + tcpSelectionKey.interestOps(SelectionKey.OP_READ); + tcpSelectionKey.selector().wakeup(); + } catch (IOException e) { + future.completeExceptionally(e); + } } } - public InetSocketAddress socks5UdpAssociateExchange(SocketChannel c) { - ByteBuffer buffer = ByteBuffer.allocate(10); - buffer.put(SOCKS5_VERSION); - buffer.put(SOCKS5_CMD_UDP_ASSOCIATE); - buffer.put(SOCKS5_RESERVED); - buffer.put(SOCKS5_ATYP_IPV4); - buffer.put(new byte[] {0, 0, 0, 0}); - buffer.putShort((short) 0x00); - buffer.flip(); - - writeToChannel(c, buffer); - buffer.clear(); + private class Socks5UserPassAuthReadHandler implements Runnable { + private final CompletableFuture future; - readFromChannel(c, buffer); - buffer.flip(); + Socks5UserPassAuthReadHandler(CompletableFuture future) { + this.future = future; + } - if (!buffer.hasRemaining()) { - throw new IllegalStateException("SOCKS5 udp associate exchange failed. No data received from server"); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + ByteBuffer buffer = ByteBuffer.allocate(2); + channel.read(buffer); + buffer.flip(); + + if (buffer.get() != SOCKS5_USER_PWD_AUTH_VERSION) { + throw new IllegalStateException("Invalid SOCKS5 user/password auth version"); + } + + byte status = buffer.get(); + if (status != 0x00) { + throw new IllegalStateException("User/password authentication failed"); + } + + if (command == Command.CONNECT) { + tcpSelectionKey.attach(new Socks5ConnectExchangeHandler(future)); + } else if (command == Command.UDP_ASSOCIATE) { + tcpSelectionKey.attach(new Socks5UdpAssociateExchangeHandler(future)); + } else { + throw new IllegalStateException("Unsupported command: " + command); + } + tcpSelectionKey.interestOps(SelectionKey.OP_WRITE); + tcpSelectionKey.selector().wakeup(); + } catch (IOException | IllegalStateException e) { + future.completeExceptionally(e); + } } + } + + private class Socks5ConnectExchangeHandler implements Runnable { + private final CompletableFuture future; - if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalStateException("Invalid SOCKS5 version"); + Socks5ConnectExchangeHandler(CompletableFuture future) { + this.future = future; } - byte reply = buffer.get(); - if (reply != SOCKS5_REP_SUCCEEDED) { - throw new IllegalStateException("UDP association failed: " + reply); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + ByteBuffer buffer; + byte addressType; + byte[] addressBytes; + + if (remote.getAddress() instanceof Inet4Address) { + addressType = SOCKS5_ATYP_IPV4; + addressBytes = remote.getAddress().getAddress(); + buffer = ByteBuffer.allocate(10); + } else if (remote.getAddress() instanceof Inet6Address) { + addressType = SOCKS5_ATYP_IPV6; + addressBytes = remote.getAddress().getAddress(); + buffer = ByteBuffer.allocate(22); + } else { + addressType = SOCKS5_ATYP_DOMAINNAME; + addressBytes = remote.getHostName().getBytes(StandardCharsets.UTF_8); + buffer = ByteBuffer.allocate(7 + addressBytes.length); + } + + buffer.put(SOCKS5_VERSION); + buffer.put(SOCKS5_CMD_CONNECT); + buffer.put(SOCKS5_RESERVED); + buffer.put(addressType); + if (addressType == SOCKS5_ATYP_DOMAINNAME) { + buffer.put((byte) addressBytes.length); + } + buffer.put(addressBytes); + buffer.putShort((short) remote.getPort()); + buffer.flip(); + channel.write(buffer); + + tcpSelectionKey.attach(new Socks5HeaderExchangeReadHandler(future)); + tcpSelectionKey.interestOps(SelectionKey.OP_READ); + tcpSelectionKey.selector().wakeup(); + } catch (IOException e) { + future.completeExceptionally(e); + } } + } - buffer.get(); // skip RSV byte + private class Socks5UdpAssociateExchangeHandler implements Runnable { + private final CompletableFuture future; - byte atyp = buffer.get(); - if (atyp != SOCKS5_ATYP_IPV4) { - throw new IllegalStateException("Invalid address type"); + Socks5UdpAssociateExchangeHandler(CompletableFuture future) { + this.future = future; } - byte[] addr = new byte[4]; - buffer.get(addr); - int port = buffer.getShort() & 0xFFFF; - return new InetSocketAddress(this.getProxyAddress().getAddress(), port); - } - - public byte[] addUdpHeader(byte[] in, InetSocketAddress to) { - ByteBuffer buffer = ByteBuffer.allocate(in.length + 10); - buffer.put(SOCKS5_VERSION); - buffer.put(SOCKS5_RESERVED); - buffer.put(SOCKS5_RESERVED); - buffer.put(SOCKS5_ATYP_IPV4); - buffer.put(to.getAddress().getAddress()); - buffer.putShort((short) to.getPort()); - buffer.put(in); - - return buffer.array(); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + ByteBuffer buffer = ByteBuffer.allocate(10); + buffer.put(SOCKS5_VERSION); + buffer.put(SOCKS5_CMD_UDP_ASSOCIATE); + buffer.put(SOCKS5_RESERVED); + // For UDP associate this is not the remote address, + // but the address where the proxy will send UDP packets after receiving them from the remote address + // there is a header for the remote address in the UDP packet + buffer.put(SOCKS5_ATYP_IPV4); + buffer.putInt(0); // 0.0.0.0 (this way it works in nat-ed networks) + buffer.putShort((short) 0); // Port 0 + buffer.flip(); + channel.write(buffer); + + tcpSelectionKey.attach(new Socks5HeaderExchangeReadHandler(future)); + tcpSelectionKey.interestOps(SelectionKey.OP_READ); + tcpSelectionKey.selector().wakeup(); + } catch (IOException e) { + future.completeExceptionally(e); + } + } } - public byte[] removeUdpHeader(byte[] in) { - byte[] out = new byte[in.length - 10]; - System.arraycopy(in, 10, out, 0, in.length - 10); - return out; - } + private class Socks5HeaderExchangeReadHandler implements Runnable { + private final CompletableFuture future; - public void socks5TcpHandshake(SocketChannel c, InetSocketAddress remote) { - byte method = this.socks5MethodSelection(c); - if (method == SOCKS5_AUTH_USER_PASS) { - this.socks5UserPwdAuthExchange(c); + Socks5HeaderExchangeReadHandler(CompletableFuture future) { + this.future = future; } - this.socks5HeaderExchange(c, remote); - } - public InetSocketAddress socks5UdpAssociateHandshake(SocketChannel c) throws UnknownHostException { - byte method = this.socks5MethodSelection(c); - if (method == SOCKS5_AUTH_USER_PASS) { - if (this.socks5User == null || this.socks5Password == null) { - throw new IllegalStateException("No user or password provided"); + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); + // Allocate 262 bytes to handle the maximum possible size of the SOCKS5 reply + ByteBuffer buffer = ByteBuffer.allocate(262); + channel.read(buffer); + buffer.flip(); + + if (buffer.get() != SOCKS5_VERSION) { + throw new IllegalStateException("Invalid SOCKS5 version"); + } + + byte reply = buffer.get(); + if (reply != SOCKS5_REP_SUCCEEDED) { + throw new IllegalStateException("Connection to remote server failed: " + reply); + } + + if (command == Command.CONNECT) { + state = State.CONNECTED; + // ignore rest of the reply + } else { + state = State.UDP_ASSOCIATE; + // get the bound port for UDP associate + /// skip reserved byte + buffer.get(); + /// read the bound address and port + byte addressType = buffer.get(); + byte[] boundAddress; + if (addressType == SOCKS5_ATYP_IPV4) { + boundAddress = new byte[4]; + } else if (addressType == SOCKS5_ATYP_IPV6) { + boundAddress = new byte[16]; + } else if (addressType == SOCKS5_ATYP_DOMAINNAME) { + int domainLength = buffer.get(); + boundAddress = new byte[domainLength]; + } else { + throw new IllegalStateException("Unsupported address type: " + addressType); + } + buffer.get(boundAddress); + // Short.toUnsignedInt makes a difference for port numbers higher than 32767 + int udpAssociatePort = Short.toUnsignedInt(buffer.getShort()); + udpChannel = DatagramChannel.open(); + udpChannel.configureBlocking(false); + udpChannel.bind(new InetSocketAddress(local.getAddress(), 0)); + udpChannel.connect(new InetSocketAddress(config.getProxyAddress().getAddress(), udpAssociatePort)); + } + + future.complete(null); + } catch (IOException e) { + future.completeExceptionally(e); } - this.socks5UserPwdAuthExchange(c); } - return this.socks5UdpAssociateExchange(c); } } diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyConfig.java b/src/main/java/org/xbill/DNS/Socks5ProxyConfig.java new file mode 100644 index 00000000..273de4de --- /dev/null +++ b/src/main/java/org/xbill/DNS/Socks5ProxyConfig.java @@ -0,0 +1,37 @@ +package org.xbill.DNS; + +import lombok.Getter; +import lombok.Setter; + +import java.net.InetSocketAddress; + +@Getter +@Setter +public class Socks5ProxyConfig { + private InetSocketAddress proxyAddress; + private AuthMethod authMethod; + private String socks5User; + private String socks5Password; + + public enum AuthMethod { + NONE, + GSSAPI, + USER_PASS + } + + public Socks5ProxyConfig(InetSocketAddress proxyAddress) { + this(proxyAddress, null, null); + authMethod = AuthMethod.NONE; + } + + public Socks5ProxyConfig(InetSocketAddress proxyAddress, String socks5User, String socks5Password) { + this.proxyAddress = proxyAddress; + this.socks5User = socks5User; + this.socks5Password = socks5Password; + authMethod = AuthMethod.USER_PASS; + } + +// public Socks5ProxyConfig(InetSocketAddress proxyAddress, GSSCredential gssCredential) { +// this(proxyAddress, null, null, gssCredential); +// } +} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java b/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java new file mode 100644 index 00000000..6284e43a --- /dev/null +++ b/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java @@ -0,0 +1,236 @@ +package org.xbill.DNS; + +import java.io.IOException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.IoClientFactory; +import org.xbill.DNS.io.TcpIoClient; +import org.xbill.DNS.io.UdpIoClient; + +@Getter +@Slf4j +public class Socks5ProxyIoClientFactory implements IoClientFactory { + + // SOCKS5 proxy configuration + private final Socks5ProxyConfig config; + + // connection pool Socks5ProxyConnection + private static final Map> socks5ConnectionPool = new ConcurrentHashMap<>(); + + // selector for handling IO events + private volatile Selector selector; + private Thread eventLoopThread; + private volatile boolean eventLoopRunning = false; + + // scheduler for handling timeouts, cleanup and closing connections + private ScheduledExecutorService timeoutScheduler; + private static final long timeout = 30000; // 30 seconds timeout + private final Map keyTimestamps = new ConcurrentHashMap<>(); + + // constructor + public Socks5ProxyIoClientFactory(Socks5ProxyConfig socks5Proxy) { + config = Objects.requireNonNull(socks5Proxy, "proxy config must not be null"); + + // start event loop if not already running + startEventLoop(); + + // Add shutdown hook for graceful shutdown + Runtime.getRuntime().addShutdownHook(new Thread(this::stopEventLoop)); + } + + // method to start the event loop + private synchronized void startEventLoop() { + try { + selector = Selector.open(); + } catch (IOException e) { + log.error("Error opening selector", e); + return; + } + + eventLoopRunning = true; + timeoutScheduler = Executors.newScheduledThreadPool(1); + eventLoopThread = new Thread(() -> { + try { + while (eventLoopRunning) { + // blocking call, waits for an io event + selector.select(); + + // get the set of keys with pending events + Set selectedKeys = selector.selectedKeys(); + Iterator keyIterator = selectedKeys.iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + keyIterator.remove(); + if (key.isValid()) { + // run the task associated with the key + ((Runnable) key.attachment()).run(); + // update the timestamp for the key + long currentTime = System.currentTimeMillis(); + keyTimestamps.put(key, currentTime); + scheduleTimeout(selector, key); + } + } + } + } catch (IOException e) { + log.error("Error in event loop", e); + } finally { + try { + if (selector != null) { + selector.close(); + } + } catch (IOException e) { + log.error("Error closing selector", e); + } + eventLoopRunning = false; + } + }); + eventLoopThread.start(); + } + + private void scheduleTimeout(Selector selector, SelectionKey key) { + timeoutScheduler.schedule(() -> { + long currentTime = System.currentTimeMillis(); + if (currentTime - keyTimestamps.getOrDefault(key, 0L) > timeout) { + log.debug("Closing connection due to timeout"); + try { + key.cancel(); + key.channel().close(); + } catch (IOException e) { + log.error("Error closing channel due to timeout", e); + } + keyTimestamps.remove(key); + selector.wakeup(); + } + }, timeout, TimeUnit.MILLISECONDS); + } + + // graceful shutdown of the event loop + public void stopEventLoop() { + // stop the event loop + eventLoopRunning = false; + if (eventLoopThread != null) { + eventLoopThread.interrupt(); + } + // stop the timeout scheduler + if (timeoutScheduler != null) { + timeoutScheduler.shutdownNow(); + } + // close all connections in the pool + for (Map subConnections : socks5ConnectionPool.values()) { + for (Socks5Proxy connection : subConnections.values()) { + try { + connection.getTcpSelectionKey().channel().close(); + connection.getTcpSelectionKey().cancel(); + } catch (IOException e) { + log.error("Error closing connection", e); + } + } + } + // close the selector + try { + selector.close(); + } catch (IOException e) { + log.error("Error closing selector", e); + } + socks5ConnectionPool.clear(); + } + + // check if the event loop thread is alive for health checks + public boolean isEventLoopThreadAlive() { + return eventLoopThread != null && eventLoopThread.isAlive(); + } + + // check if the timeout scheduler is running for health checks + public boolean isTimeoutSchedulerRunning() { + return timeoutScheduler != null && !timeoutScheduler.isShutdown(); + } + + // check if the event loop is healthy overall + public boolean isEventLoopHealthy() { + return isEventLoopThreadAlive() && isTimeoutSchedulerRunning(); + } + + // register a new connection to the selector + public SelectionKey registerToSelector(SelectableChannel conn) throws IOException { + return conn.register(selector, SelectionKey.OP_CONNECT); + } + + // unregister a connection from the selector + public synchronized void unregisterFromSelector( + PoolConn poolConn, + Throwable ex) throws IOException { + if ( + ex == null + && poolConn.getSocks5Conn().getTcpSelectionKey().isValid() + && poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen() + ) { + // unregister for reuse + poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); + } else { + // clean up the socks connection instance in case of an exception or invalid state + cleanupConnectionFromPool(poolConn); + } + } + + public synchronized PoolConn getPoolConnFromPool(String connectionID) { + Map subConnections = socks5ConnectionPool.get(connectionID); + if (subConnections != null && !subConnections.isEmpty()) { + for (Map.Entry entry : subConnections.entrySet()) { + Socks5Proxy socks5Conn = entry.getValue(); + if (socks5Conn.getTcpSelectionKey().channel().isOpen() + && !socks5Conn.getTcpSelectionKey().channel().isRegistered()) { + return new PoolConn(connectionID, entry.getKey(), socks5Conn); + } + } + } + return null; + } + + @Getter + @RequiredArgsConstructor + public static class PoolConn { + private final String connectionID; + private final String subConnectionID; + private final Socks5Proxy socks5Conn; + } + + public synchronized PoolConn addConnectionToPool(String connectionID, Socks5Proxy socks5Conn) { + String subConnectionID = UUID.randomUUID().toString(); + socks5ConnectionPool.computeIfAbsent(connectionID, k -> new ConcurrentHashMap<>()).put(subConnectionID, socks5Conn); + return new PoolConn(connectionID, subConnectionID, socks5Conn); + } + + public synchronized void cleanupConnectionFromPool(PoolConn poolConn) throws IOException { + if (poolConn.getSocks5Conn() != null && poolConn.getSocks5Conn().getTcpSelectionKey() != null) { + poolConn.getSocks5Conn().getTcpSelectionKey().channel().close(); + poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); + } + Map subConnections = socks5ConnectionPool.get(poolConn.getConnectionID()); + if (subConnections != null) { + subConnections.remove(poolConn.getSubConnectionID()); + if (subConnections.isEmpty()) { + socks5ConnectionPool.remove(poolConn.getConnectionID()); + } + } + } + + @Override + public TcpIoClient createOrGetTcpClient() { + return new Socks5ProxyTcpIoClient(this, config); + } + + @Override + public UdpIoClient createOrGetUdpClient() { + return new Socks5ProxyUdpIoClient(this, config); + } +} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java b/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java new file mode 100644 index 00000000..698e4abc --- /dev/null +++ b/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java @@ -0,0 +1,156 @@ +package org.xbill.DNS; + +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.TcpIoClient; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +@Slf4j +public class Socks5ProxyTcpIoClient implements TcpIoClient { + private final Socks5ProxyIoClientFactory factory; + private final Socks5ProxyConfig config; + private Socks5ProxyIoClientFactory.PoolConn poolConn; + + public Socks5ProxyTcpIoClient(Socks5ProxyIoClientFactory factory, Socks5ProxyConfig config) { + this.factory = factory; + this.config = config; + } + + public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { + Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); + try { + if (poolConn == null || !poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen()) { + SocketChannel tcpConn = SocketChannel.open(); + tcpConn.configureBlocking(false); + SelectionKey selectionKey = factory.registerToSelector(tcpConn); + Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.CONNECT); + tcpConn.connect(config.getProxyAddress()); + socks5Conn.handleSOCKS5(f); + this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); + } else { + SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); + poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); + this.poolConn = poolConn; + f.complete(null); + } + } catch (IOException e) { + f.completeExceptionally(e); + } + } + + @Override + public CompletableFuture sendAndReceiveTcp( + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + Duration timeout) { + if (local == null) { + local = new InetSocketAddress(0); + } + // keyString is used to identify and reuse SOCKS5 connections + String keyString = local.toString() + "-" + remote.toString() + "-TCP"; + CompletableFuture socksF = new CompletableFuture<>(); + this.initOrReuseConn(socksF, keyString, local, remote); + + return socksF.thenComposeAsync(v -> { + CompletableFuture dataF = new CompletableFuture<>(); + try { + poolConn.getSocks5Conn().getTcpSelectionKey().attach(new SendHandler(dataF, data, poolConn.getSocks5Conn().getTcpSelectionKey())); + poolConn.getSocks5Conn().getTcpSelectionKey().interestOps(SelectionKey.OP_WRITE); + poolConn.getSocks5Conn().getTcpSelectionKey().selector().wakeup(); + } catch (Exception e) { + dataF.completeExceptionally(e); + } + return dataF; + }).whenComplete((result, ex) -> { + try { + factory.unregisterFromSelector(poolConn, ex); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + private class SendHandler implements Runnable { + private final CompletableFuture future; + private final byte[] data; + private final SelectionKey selectionKey; + + public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { + this.future = future; + this.data = data; + this.selectionKey = selectionKey; + } + + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) selectionKey.channel(); + ByteBuffer buffer = ByteBuffer.allocate(data.length + 2); + buffer.put((byte) (data.length >>> 8)); + buffer.put((byte) (data.length & 0xFF)); + buffer.put(data); + buffer.flip(); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + selectionKey.attach(new ReceiveHandler(future, selectionKey)); + selectionKey.interestOps(SelectionKey.OP_READ); + selectionKey.selector().wakeup(); + } catch (IOException e) { + future.completeExceptionally(e); + } + } + } + + private class ReceiveHandler implements Runnable { + private final CompletableFuture future; + private final SelectionKey selectionKey; + private final ByteBuffer responseLengthData = ByteBuffer.allocate(2); + private ByteBuffer responseData; + + public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey) { + this.future = future; + this.selectionKey = selectionKey; + } + + @Override + public void run() { + try { + SocketChannel channel = (SocketChannel) selectionKey.channel(); + if (responseData == null) { + int read = channel.read(responseLengthData); + if (read < 0) { + throw new IOException("Connection closed by peer"); + } + if (responseLengthData.position() == 2) { + responseLengthData.flip(); + int length = ((responseLengthData.get(0) & 0xFF) << 8) + (responseLengthData.get(1) & 0xFF); + responseData = ByteBuffer.allocate(length); + } + } + if (responseData != null) { + int read = channel.read(responseData); + if (read < 0) { + throw new IOException("Connection closed by peer"); + } + if (!responseData.hasRemaining()) { + responseData.flip(); + byte[] data = new byte[responseData.limit()]; + responseData.get(data); + future.complete(data); + } + } + } catch (IOException e) { + future.completeExceptionally(e); + } + } + } +} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java b/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java new file mode 100644 index 00000000..b8243074 --- /dev/null +++ b/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java @@ -0,0 +1,158 @@ +package org.xbill.DNS; + +import org.xbill.DNS.io.UdpIoClient; + +import java.io.EOFException; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class Socks5ProxyUdpIoClient implements UdpIoClient { + private final Socks5ProxyIoClientFactory factory; + private Socks5ProxyIoClientFactory.PoolConn poolConn; + private SelectionKey udpSelectionKey; + private final Socks5ProxyConfig config; + private int max; + + public Socks5ProxyUdpIoClient( + Socks5ProxyIoClientFactory factory, + Socks5ProxyConfig config) { + this.factory = factory; + this.config = config; + } + + public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { + Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); + try { + if (poolConn == null) { + SocketChannel tcpConn = SocketChannel.open(); + tcpConn.configureBlocking(false); + SelectionKey selectionKey = factory.registerToSelector(tcpConn); + Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.UDP_ASSOCIATE); + tcpConn.connect(config.getProxyAddress()); + socks5Conn.handleSOCKS5(f); + this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); + } else { + SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); + poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); + this.poolConn = poolConn; + f.complete(null); + } + } catch (IOException e) { + f.completeExceptionally(e); + } + } + + + @Override + public CompletableFuture sendAndReceiveUdp( + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + int max, + Duration timeout) { + this.max = max; + InetSocketAddress finalLocal; + if (local == null) { + finalLocal = new InetSocketAddress(0); + } else { + finalLocal = local; + } + // keyString is used to identify and reuse SOCKS5 connections + String keyString = finalLocal.toString() + "-" + remote.toString() + "-UDP"; + CompletableFuture socksF = new CompletableFuture<>(); + this.initOrReuseConn(socksF, keyString, finalLocal, remote); + + + return socksF.thenComposeAsync(v -> { + CompletableFuture dataF = new CompletableFuture<>(); + try { + udpSelectionKey = this.poolConn.getSocks5Conn().getUdpChannel().register(factory.getSelector(), SelectionKey.OP_READ); + udpSelectionKey.selector().wakeup(); + + udpSelectionKey.attach(new SendHandler(dataF, data, udpSelectionKey)); + udpSelectionKey.interestOps(SelectionKey.OP_WRITE); + udpSelectionKey.selector().wakeup(); + } catch (Exception e) { + dataF.completeExceptionally(e); + } + return dataF; + }).whenComplete((result, ex) -> { + try { + factory.unregisterFromSelector(poolConn, ex); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + private class SendHandler implements Runnable { + private final CompletableFuture future; + private final byte[] data; + private final SelectionKey selectionKey; + + public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { + this.future = future; + this.data = data; + this.selectionKey = selectionKey; + } + + @Override + public void run() { + try { + DatagramChannel channel = (DatagramChannel) selectionKey.channel(); + ByteBuffer buffer = poolConn.getSocks5Conn().addSocks5UdpAssociateHeader(data); + int headerLength = buffer.position()-data.length; + buffer.flip(); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + selectionKey.attach(new ReceiveHandler(future, selectionKey, headerLength)); + selectionKey.interestOps(SelectionKey.OP_READ); + selectionKey.selector().wakeup(); + } catch (IOException e) { + future.completeExceptionally(e); + } + } + } + + private class ReceiveHandler implements Runnable { + private final CompletableFuture future; + private final SelectionKey selectionKey; + private final int headerLength; + + public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey, int headerLength) { + this.future = future; + this.selectionKey = selectionKey; + this.headerLength = headerLength; + } + + @Override + public void run() { + try { + DatagramChannel channel = (DatagramChannel) selectionKey.channel(); + ByteBuffer responseData = ByteBuffer.allocate(max); + int read = channel.read(responseData); + if (read < 0) { + throw new EOFException(); + } + + int length = responseData.position() - headerLength; + byte[] data = new byte[length]; + responseData.position(headerLength); + responseData.get(data, 0, length); + future.complete(data); + + selectionKey.cancel(); + } catch (IOException e) { + future.completeExceptionally(e); + } + } + } +} From 156c797e5cc8b12eccb30adcead1c8f1d2a20e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Thu, 5 Dec 2024 18:27:36 +0100 Subject: [PATCH 06/53] clean up --- src/main/java/org/xbill/DNS/SimpleResolver.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/xbill/DNS/SimpleResolver.java b/src/main/java/org/xbill/DNS/SimpleResolver.java index dc919830..4f5fd72a 100644 --- a/src/main/java/org/xbill/DNS/SimpleResolver.java +++ b/src/main/java/org/xbill/DNS/SimpleResolver.java @@ -389,14 +389,14 @@ CompletableFuture sendAsync(Message query, boolean forceTcp, Executor e CompletableFuture result; if (tcp) { result = - ioClientFactory - .createOrGetTcpClient() - .sendAndReceiveTcp(localAddress, address, query, out, timeoutValue); + ioClientFactory + .createOrGetTcpClient() + .sendAndReceiveTcp(localAddress, address, query, out, timeoutValue); } else { result = - ioClientFactory - .createOrGetUdpClient() - .sendAndReceiveUdp(localAddress, address, query, out, udpSize, timeoutValue); + ioClientFactory + .createOrGetUdpClient() + .sendAndReceiveUdp(localAddress, address, query, out, udpSize, timeoutValue); } return result.thenComposeAsync( From 3dae1483b64844de58d671ad48d1710ffd1cd815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Fri, 10 Jan 2025 21:36:02 +0100 Subject: [PATCH 07/53] integration of SOCKS5 with the current selector thread and timeout implementation --- .../org/xbill/DNS/NioSocks5ProxyFactory.java | 49 ++ .../java/org/xbill/DNS/NioSocksHandler.java | 330 ++++++++++++ .../java/org/xbill/DNS/NioSocksTcpClient.java | 37 ++ .../java/org/xbill/DNS/NioSocksUdpClient.java | 83 +++ src/main/java/org/xbill/DNS/NioTcpClient.java | 325 +----------- .../java/org/xbill/DNS/NioTcpHandler.java | 392 +++++++++++++++ src/main/java/org/xbill/DNS/NioUdpClient.java | 206 +------- .../java/org/xbill/DNS/NioUdpHandler.java | 262 ++++++++++ src/main/java/org/xbill/DNS/Socks5Proxy.java | 37 +- .../xbill/DNS/Socks5ProxyIoClientFactory.java | 472 +++++++++--------- .../org/xbill/DNS/Socks5ProxyTcpIoClient.java | 312 ++++++------ .../org/xbill/DNS/Socks5ProxyUdpIoClient.java | 316 ++++++------ 12 files changed, 1731 insertions(+), 1090 deletions(-) create mode 100644 src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java create mode 100644 src/main/java/org/xbill/DNS/NioSocksHandler.java create mode 100644 src/main/java/org/xbill/DNS/NioSocksTcpClient.java create mode 100644 src/main/java/org/xbill/DNS/NioSocksUdpClient.java create mode 100644 src/main/java/org/xbill/DNS/NioTcpHandler.java create mode 100644 src/main/java/org/xbill/DNS/NioUdpHandler.java diff --git a/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java b/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java new file mode 100644 index 00000000..48783bfb --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java @@ -0,0 +1,49 @@ +package org.xbill.DNS; + +import java.io.IOException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.IoClientFactory; +import org.xbill.DNS.io.TcpIoClient; +import org.xbill.DNS.io.UdpIoClient; + +@Getter +@Slf4j +public class NioSocks5ProxyFactory implements IoClientFactory { + + // SOCKS5 proxy configuration + private final Socks5ProxyConfig config; + + // io clients + private final TcpIoClient tcpIoClient; + private final UdpIoClient udpIoClient; + + // constructor + public NioSocks5ProxyFactory(Socks5ProxyConfig socks5Proxy) { + config = Objects.requireNonNull(socks5Proxy, "proxy config must not be null"); + tcpIoClient = new NioSocksTcpClient(config); + udpIoClient = new NioSocksUdpClient(config); + } + + + @Override + public TcpIoClient createOrGetTcpClient() { + return tcpIoClient; + } + + @Override + public UdpIoClient createOrGetUdpClient() { + return udpIoClient; + } + +} diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java new file mode 100644 index 00000000..900c6d67 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -0,0 +1,330 @@ +package org.xbill.DNS; + +import lombok.Getter; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +@Getter +public class NioSocksHandler { + private static final byte SOCKS5_VERSION = 0x05; + private static final byte SOCKS5_USER_PWD_AUTH_VERSION = 0x01; + private static final byte SOCKS5_AUTH_NONE = 0x00; + private static final byte SOCKS5_AUTH_GSSAPI = 0x01; + private static final byte SOCKS5_AUTH_USER_PASS = 0x02; + private static final byte SOCKS5_AUTH_NO_ACCEPTABLE_METHODS = (byte) 0xFF; + + public static final byte SOCKS5_CMD_CONNECT = 0x01; + public static final byte SOCKS5_CMD_BIND = 0x02; + public static final byte SOCKS5_CMD_UDP_ASSOCIATE = 0x03; + + private static final byte SOCKS5_ATYP_IPV4 = 0x01; + private static final byte SOCKS5_ATYP_DOMAINNAME = 0x03; + private static final byte SOCKS5_ATYP_IPV6 = 0x04; + + private static final byte SOCKS5_REP_SUCCEEDED = 0x00; + private static final byte SOCKS5_REP_GENERAL_FAILURE = 0x01; + private static final byte SOCKS5_REP_CONNECTION_NOT_ALLOWED = 0x02; + private static final byte SOCKS5_REP_NETWORK_UNREACHABLE = 0x03; + private static final byte SOCKS5_REP_HOST_UNREACHABLE = 0x04; + private static final byte SOCKS5_REP_CONNECTION_REFUSED = 0x05; + private static final byte SOCKS5_REP_TTL_EXPIRED = 0x06; + private static final byte SOCKS5_REP_COMMAND_NOT_SUPPORTED = 0x07; + private static final byte SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED = 0x08; + + private static final byte SOCKS5_RESERVED = 0x00; + + private final InetSocketAddress remoteAddress; + private final InetSocketAddress localAddress; + private final InetSocketAddress proxyAddress; + private final String socks5User; + private final String socks5Password; + + public NioSocksHandler(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress, String socks5User, String socks5Password) { + this.remoteAddress = Objects.requireNonNull(remoteAddress, "remoteAddress must not be null"); + this.localAddress = localAddress; //Objects.requireNonNull(localAddress, "localAddress must not be null"); + this.proxyAddress = Objects.requireNonNull(proxyAddress, "proxyAddress must not be null"); + this.socks5User = socks5User; + this.socks5Password = socks5Password; + } + + public NioSocksHandler(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress) { + this(proxyAddress, remoteAddress, localAddress, null, null); + } + + private MethodSelectionRequest getMethodSelectionRequest() { + return new MethodSelectionRequest((this.socks5User != null && this.socks5Password != null) ? SOCKS5_AUTH_USER_PASS : SOCKS5_AUTH_NONE); + } + + public CompletableFuture doAuthHandshake(NioTcpHandler.ChannelState channel, Message query, long endTime) { + CompletableFuture authHandshakeF = new CompletableFuture<>(); + + // SOCKS5 method selection transaction + CompletableFuture methodSelectionF = new CompletableFuture<>(); + NioSocksHandler.MethodSelectionRequest methodSelectionRequest = getMethodSelectionRequest(); + NioTcpHandler.Transaction methodSelectionTransaction = new NioTcpHandler.Transaction( + query, methodSelectionRequest.toBytes(), endTime, channel.getChannel(), methodSelectionF); + channel.queueTransaction(methodSelectionTransaction); + methodSelectionF.thenComposeAsync( + methodSelectionBytes -> { + if (methodSelectionBytes.length != 2) { + authHandshakeF.completeExceptionally(new UnsupportedOperationException("Invalid SOCKS5 method selection response")); + } + NioSocksHandler.MethodSelectionResponse methodSelectionResponse = new NioSocksHandler.MethodSelectionResponse(methodSelectionBytes); + if (methodSelectionResponse.getMethod() == NioSocksHandler.SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { + authHandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); + } else { + if (methodSelectionResponse.getMethod() == NioSocksHandler.SOCKS5_AUTH_NONE) { + authHandshakeF.complete(null); + } +// else if (methodSelectionResponse.getMethod() == NioSocksHandler.SOCKS5_AUTH_USER_PASS) { +// // SOCKS5 authentication transaction (if required) +// CompletableFuture userPassAuthF = new CompletableFuture<>(); +// UserPassAuthRequest userPassAuthRequest = getUserPassAuthRequest(); +// NioTcpHandler.Transaction userPwdAuthTransaction = NioTcpHandler.Transaction(query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassF); +// channel.queueTransaction(userPwdAuthTransaction); +// userPassAuthF.thenComposeAsync( +// authIn -> { +// CompletableFuture authF = new CompletableFuture<>(); +// UserPwdAuthResponse userPwdAuthResponse = UserPwdAuthResponse.fromBytes(authIn); +// if (userPwdAuthResponse.getStatus() != NioSocksHandler.SOCKS5_REP_SUCCEEDED) { +// authHandshakeF.completeExceptionally( +// new UnsupportedOperationException("SOCKS5 user/pwd authentication failed with status: " + userPwdAuthResponse.getStatus())); +// } else { +// authF.complete(authIn); +// } +// return authF; +// } +// ); +// } + } + return null; + } + ); + + return authHandshakeF; + } + + public CompletableFuture doConnectHandshake(NioTcpHandler.ChannelState channel, Message query, long endTime) { + CompletableFuture cmdHandshakeF = new CompletableFuture<>(); + + // SOCKS5 cmd transaction + CompletableFuture commandF = new CompletableFuture<>(); + CmdRequest cmdRequest = new CmdRequest(SOCKS5_CMD_CONNECT, remoteAddress); + NioTcpHandler.Transaction commandTransaction = new NioTcpHandler.Transaction( + query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); + channel.queueTransaction(commandTransaction); + commandF.thenComposeAsync( + in -> { + CmdResponse cmdResponse = new CmdResponse(in); + if (cmdResponse.getReply() != NioSocksHandler.SOCKS5_REP_SUCCEEDED) { + cmdHandshakeF.completeExceptionally( + new UnsupportedOperationException("SOCKS5 command failed with status: " + cmdResponse.getReply())); + } else { + cmdHandshakeF.complete(in); + } + return null; + } + ); + + return cmdHandshakeF; + } + public CompletableFuture doUdpAssociateHandshake(NioTcpHandler.ChannelState channel, Message query, long endTime) { + CompletableFuture cmdHandshakeF = new CompletableFuture<>(); + + // SOCKS5 cmd transaction + CompletableFuture commandF = new CompletableFuture<>(); + CmdRequest cmdRequest = new CmdRequest(SOCKS5_CMD_UDP_ASSOCIATE, new InetSocketAddress("0.0.0.0", 0)); + NioTcpHandler.Transaction commandTransaction = new NioTcpHandler.Transaction( + query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); + channel.queueTransaction(commandTransaction); + commandF.thenComposeAsync( + in -> { + CmdResponse cmdResponse = new CmdResponse(in); + if (cmdResponse.getReply() != NioSocksHandler.SOCKS5_REP_SUCCEEDED) { + cmdHandshakeF.completeExceptionally( + new UnsupportedOperationException("SOCKS5 command failed with status: " + cmdResponse.getReply())); + } else { + cmdHandshakeF.complete(in); + } + return null; + } + ); + + return cmdHandshakeF; + } + + public synchronized CompletableFuture doSocks5Handshake(NioTcpHandler.ChannelState channel, byte socks5Cmd, Message query, long endTime) { + CompletableFuture socks5HandshakeF = new CompletableFuture<>(); + channel.setSocks5(true); + + CompletableFuture authHandshakeF = doAuthHandshake(channel, query, endTime); + authHandshakeF.thenRunAsync( + () -> { + CompletableFuture cmdHandshakeF; + if (socks5Cmd == SOCKS5_CMD_CONNECT) { + cmdHandshakeF = doConnectHandshake(channel, query, endTime); + } else if (socks5Cmd == SOCKS5_CMD_UDP_ASSOCIATE) { + cmdHandshakeF = doUdpAssociateHandshake(channel, query, endTime); + } else { + cmdHandshakeF = CompletableFuture.failedFuture(new UnsupportedOperationException("Unsupported SOCKS5 command: " + socks5Cmd)); + } + cmdHandshakeF.thenComposeAsync( + in -> { + socks5HandshakeF.complete(in); + return null; + } + ).exceptionally( + e -> { + socks5HandshakeF.completeExceptionally(e); + return null; + } + ); + } + ).exceptionally( + e -> { + socks5HandshakeF.completeExceptionally(e); + return null; + } + ); + + return socks5HandshakeF; + } + + public byte[] addUdpHeader(byte[] data, InetSocketAddress to) { + ByteBuffer buffer; + byte addressType; + byte[] addressBytes; + if (remoteAddress.getAddress() instanceof Inet4Address) { + addressType = SOCKS5_ATYP_IPV4; + addressBytes = remoteAddress.getAddress().getAddress(); + buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); + } else if (remoteAddress.getAddress() instanceof Inet6Address) { + addressType = SOCKS5_ATYP_IPV6; + addressBytes = remoteAddress.getAddress().getAddress(); + buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); + } else { + addressType = SOCKS5_ATYP_DOMAINNAME; + addressBytes = remoteAddress.getHostName().getBytes(StandardCharsets.UTF_8); + buffer = ByteBuffer.allocate(4 + 1 + addressBytes.length + 2 + data.length); + } + + buffer.put((byte) 0x00); // RSV + buffer.put((byte) 0x00); // RSV + buffer.put((byte) 0x00); // FRAG + buffer.put(addressType); // ATYP (IPv4) + if (addressType == SOCKS5_ATYP_DOMAINNAME) { + buffer.put((byte) addressBytes.length); + } + buffer.put(addressBytes); // DST.ADDR + buffer.putShort((short) remoteAddress.getPort()); // DST.PORT + buffer.put(data); // DATA + + return buffer.array(); + } + + public byte[] removeUdpHeader(byte[] in) { + byte[] out = new byte[in.length - 10]; + System.arraycopy(in, 10, out, 0, in.length - 10); + return out; + } + + + static class MethodSelectionRequest { + private final byte version; + private final byte method; + + public MethodSelectionRequest(byte method) { + this.version = SOCKS5_VERSION; + this.method = method; + } + public byte[] toBytes() { + ByteBuffer buffer = ByteBuffer.allocate(3); + buffer.put(this.version); + buffer.put((byte) 0x01); + buffer.put(this.method); + return buffer.array(); + } + } + + @Getter + static class MethodSelectionResponse { + private final byte version; + private final byte method; + + public MethodSelectionResponse(byte[] methodSelectionBytes) { + ByteBuffer buffer = ByteBuffer.wrap(methodSelectionBytes); + version = buffer.get();; + method = buffer.get();; + } + } + + static class CmdRequest { + private final byte version; + private final byte command; + private final byte reserved; + private final byte addressType; + private final byte[] addressBytes; + private final short port; + + private final int bufferSize; + + public CmdRequest(byte command, InetSocketAddress address) { + version = SOCKS5_VERSION; + this.command = command; + reserved = SOCKS5_RESERVED; + if (address.getAddress() instanceof Inet4Address) { + addressType = SOCKS5_ATYP_IPV4; + addressBytes = address.getAddress().getAddress(); + bufferSize = 10; + } else if (address.getAddress() instanceof Inet6Address) { + addressType = SOCKS5_ATYP_IPV6; + addressBytes = address.getAddress().getAddress(); + bufferSize = 22; + } else { + addressType = SOCKS5_ATYP_DOMAINNAME; + addressBytes = address.getHostName().getBytes(StandardCharsets.UTF_8); + bufferSize = 7 + addressBytes.length; + } + port = (short) address.getPort(); + } + + public byte[] toBytes() { + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + buffer.put(this.version); + buffer.put(this.command); + buffer.put(this.reserved); + buffer.put(this.addressType); + buffer.put(this.addressBytes); + buffer.putShort(this.port); + return buffer.array(); + } + } + + @Getter + static class CmdResponse { + private final byte version; + private final byte reply; + private final byte reserved; + private final byte addressType; + private final byte[] address; + private final int port; + + public CmdResponse(byte[] commandResponseBytes) { + ByteBuffer buffer = ByteBuffer.wrap(commandResponseBytes); + version = buffer.get(); + reply = buffer.get(); + reserved = buffer.get(); + addressType = buffer.get(); + address = new byte[addressType == SOCKS5_ATYP_IPV4 ? 4 : 16]; + buffer.get(address); + // Short.toUnsignedInt makes a difference for port numbers higher than 32767 + port = Short.toUnsignedInt(buffer.getShort()); + } + } +} diff --git a/src/main/java/org/xbill/DNS/NioSocksTcpClient.java b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java new file mode 100644 index 00000000..eedf9365 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.TcpIoClient; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +@Slf4j +final class NioSocksTcpClient extends NioTcpHandler implements TcpIoClient { + // TCP handler + private final NioTcpHandler tcpHandler; + // SOCKS5 proxy configuration + private final Socks5ProxyConfig socksConfig; + + NioSocksTcpClient(Socks5ProxyConfig config) { + socksConfig = Objects.requireNonNull(config, "proxy config must not be null"); + tcpHandler = new NioTcpHandler(); + } + + @Override + public CompletableFuture sendAndReceiveTcp( + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + Duration timeout) { + NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local); + return tcpHandler.sendAndReceiveTcp(local, remote, proxy, query, data, timeout); + } +} diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java new file mode 100644 index 00000000..11960159 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.UdpIoClient; + +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.nio.channels.DatagramChannel; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +final class NioSocksUdpClient extends NioClient implements UdpIoClient { + private static final NioTcpHandler tcpHandler = new NioTcpHandler(); + private static final NioUdpHandler udpHandler = new NioUdpHandler(); + private final Socks5ProxyConfig socksConfig; + private static final Map channelMap = new ConcurrentHashMap<>(); + + NioSocksUdpClient(Socks5ProxyConfig config) { + socksConfig = config; + } + + @Override + public CompletableFuture sendAndReceiveUdp( + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + int max, + Duration timeout) { + CompletableFuture f = new CompletableFuture<>(); + long endTime = System.nanoTime() + timeout.toNanos(); + NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local); + NioTcpHandler.ChannelState tcpChannel = tcpHandler.createOrGetChannelState(local, remote, proxy, f); + + synchronized (tcpChannel) { + if (tcpChannel.socks5HandshakeF == null) { + tcpChannel.setSocks5(true); + tcpChannel.socks5HandshakeF = proxy.doSocks5Handshake(tcpChannel, NioSocksHandler.SOCKS5_CMD_UDP_ASSOCIATE, query, endTime); + } + tcpChannel.socks5HandshakeF.thenComposeAsync( + cmdBytes -> { + NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); + // newRemote is the UDP associate address + InetSocketAddress newRemote = new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); + byte[] wrappedData = proxy.addUdpHeader(data, newRemote); + DatagramChannel udpChannel = channelMap.computeIfAbsent(newRemote.toString(), k -> { + try { + return udpHandler.createChannel(local, newRemote, f); + } catch (Exception e) { + log.error("Failed to open UDP socket", e); + return null; + } + }); + udpHandler.sendAndReceiveUdp(local, newRemote, udpChannel, query, wrappedData, max, timeout).thenApplyAsync( + response -> { + if (response.length < 10) { + channelMap.remove(newRemote.toString()); + f.completeExceptionally(new IllegalStateException("SOCKS5 UDP response too short")); + } else { + // remove the SOCKS5 header from UDP response + f.complete(proxy.removeUdpHeader(response)); + } + return null; + } + ).exceptionally(ex -> { + channelMap.remove(newRemote.toString()); + f.completeExceptionally(ex); + return null; + }); + return CompletableFuture.completedFuture(null); + } + ).exceptionally(ex -> { + f.completeExceptionally(ex); + return null; + }); + return f; + } + } +} diff --git a/src/main/java/org/xbill/DNS/NioTcpClient.java b/src/main/java/org/xbill/DNS/NioTcpClient.java index 566c00eb..fa53bbe9 100644 --- a/src/main/java/org/xbill/DNS/NioTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioTcpClient.java @@ -1,288 +1,19 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import java.io.EOFException; -import java.io.IOException; import java.net.InetSocketAddress; -import java.net.SocketTimeoutException; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; import java.time.Duration; -import java.util.Iterator; -import java.util.Map; -import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.io.TcpIoClient; @Slf4j -final class NioTcpClient extends NioClient implements TcpIoClient { - private final Queue registrationQueue = new ConcurrentLinkedQueue<>(); - private final Map channelMap = new ConcurrentHashMap<>(); +final class NioTcpClient extends NioTcpHandler implements TcpIoClient { + NioTcpHandler tcpHandler; NioTcpClient() { - setRegistrationsTask(this::processPendingRegistrations, true); - setTimeoutTask(this::checkTransactionTimeouts, true); - setCloseTask(this::closeTcp, true); - } - - private void processPendingRegistrations() { - while (!registrationQueue.isEmpty()) { - ChannelState state = registrationQueue.poll(); - if (state == null) { - continue; - } - - try { - final Selector selector = selector(); - if (!state.channel.isConnected()) { - state.channel.register(selector, SelectionKey.OP_CONNECT, state); - } else { - state.channel.keyFor(selector).interestOps(SelectionKey.OP_WRITE); - } - } catch (IOException e) { - state.handleChannelException(e); - } - } - } - - private void checkTransactionTimeouts() { - for (ChannelState state : channelMap.values()) { - for (Iterator it = state.pendingTransactions.iterator(); it.hasNext(); ) { - Transaction t = it.next(); - if (t.endTime - System.nanoTime() < 0) { - t.f.completeExceptionally(new SocketTimeoutException("Query timed out")); - it.remove(); - } - } - } - } - - private void closeTcp() { - registrationQueue.clear(); - EOFException closing = new EOFException("Client is closing"); - channelMap.forEach( - (key, state) -> { - state.handleTransactionException(closing); - state.handleChannelException(closing); - }); - channelMap.clear(); - } - - @RequiredArgsConstructor - private static class Transaction { - private final Message query; - private final byte[] queryData; - private final long endTime; - private final SocketChannel channel; - private final CompletableFuture f; - private ByteBuffer queryDataBuffer; - long bytesWrittenTotal = 0; - - boolean send() throws IOException { - // send can be invoked multiple times if the entire buffer couldn't be written at once - if (bytesWrittenTotal == queryData.length + 2) { - return true; - } - - if (queryDataBuffer == null) { - // combine length+message to avoid multiple TCP packets - // https://datatracker.ietf.org/doc/html/rfc7766#section-8 - queryDataBuffer = ByteBuffer.allocate(queryData.length + 2); - queryDataBuffer.put((byte) (queryData.length >>> 8)); - queryDataBuffer.put((byte) (queryData.length & 0xFF)); - queryDataBuffer.put(queryData); - queryDataBuffer.flip(); - } - - verboseLog( - "TCP write: transaction id=" + query.getHeader().getID(), - channel.socket().getLocalSocketAddress(), - channel.socket().getRemoteSocketAddress(), - queryDataBuffer); - - while (queryDataBuffer.hasRemaining()) { - long bytesWritten = channel.write(queryDataBuffer); - bytesWrittenTotal += bytesWritten; - if (bytesWritten == 0) { - log.debug( - "Insufficient room for the data in the underlying output buffer for transaction {}, retrying", - query.getHeader().getID()); - return false; - } else if (bytesWrittenTotal < queryData.length) { - log.debug( - "Wrote {} of {} bytes data for transaction {}", - bytesWrittenTotal, - queryData.length, - query.getHeader().getID()); - } - } - - log.debug( - "Send for transaction {} is complete, wrote {} bytes", - query.getHeader().getID(), - bytesWrittenTotal); - return true; - } - } - - @RequiredArgsConstructor - private class ChannelState implements KeyProcessor { - private final SocketChannel channel; - final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); - ByteBuffer responseLengthData = ByteBuffer.allocate(2); - ByteBuffer responseData = ByteBuffer.allocate(Message.MAXLENGTH); - int readState = 0; - - @Override - public void processReadyKey(SelectionKey key) { - if (key.isValid()) { - if (key.isConnectable()) { - processConnect(key); - } else { - if (key.isWritable()) { - processWrite(key); - } - if (key.isReadable()) { - processRead(); - } - } - } - } - - void handleTransactionException(IOException e) { - for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { - Transaction t = it.next(); - t.f.completeExceptionally(e); - it.remove(); - } - } - - private void handleChannelException(IOException e) { - handleTransactionException(e); - for (Map.Entry entry : channelMap.entrySet()) { - if (entry.getValue() == this) { - channelMap.remove(entry.getKey()); - try { - channel.close(); - } catch (IOException ex) { - log.warn( - "Failed to close channel l={}/r={}", - entry.getKey().local, - entry.getKey().remote, - ex); - } - return; - } - } - } - - private void processConnect(SelectionKey key) { - try { - channel.finishConnect(); - key.interestOps(SelectionKey.OP_WRITE); - } catch (IOException e) { - handleChannelException(e); - } - } - - private void processRead() { - try { - if (readState == 0) { - int read = channel.read(responseLengthData); - if (read < 0) { - handleChannelException(new EOFException()); - return; - } - - if (responseLengthData.position() == 2) { - int length = - ((responseLengthData.get(0) & 0xFF) << 8) + (responseLengthData.get(1) & 0xFF); - responseLengthData.flip(); - responseData.limit(length); - readState = 1; - } - } - - int read = channel.read(responseData); - if (read < 0) { - handleChannelException(new EOFException()); - return; - } else if (responseData.hasRemaining()) { - return; - } - } catch (IOException e) { - handleChannelException(e); - return; - } - - readState = 0; - responseData.flip(); - byte[] data = new byte[responseData.limit()]; - System.arraycopy( - responseData.array(), responseData.arrayOffset(), data, 0, responseData.limit()); - - // The message was shorter than the minimum length to find the transaction, abort - if (data.length < 2) { - verboseLog( - "TCP read: response too short for a valid reply, discarding", - channel.socket().getLocalSocketAddress(), - channel.socket().getRemoteSocketAddress(), - data); - return; - } - - int id = ((data[0] & 0xFF) << 8) + (data[1] & 0xFF); - verboseLog( - "TCP read: transaction id=" + id, - channel.socket().getLocalSocketAddress(), - channel.socket().getRemoteSocketAddress(), - data); - - for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { - Transaction t = it.next(); - int qid = t.query.getHeader().getID(); - if (id == qid) { - t.f.complete(data); - it.remove(); - return; - } - } - - log.warn("Transaction for answer to id {} not found", id); - } - - private void processWrite(SelectionKey key) { - for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { - Transaction t = it.next(); - try { - if (!t.send()) { - // Write was incomplete because the output buffer was full. Wait until the selector - // tells us that we can write again - key.interestOps(SelectionKey.OP_WRITE); - return; - } - } catch (IOException e) { - t.f.completeExceptionally(e); - it.remove(); - } - } - - key.interestOps(SelectionKey.OP_READ); - } - } - - @RequiredArgsConstructor - @EqualsAndHashCode - private static class ChannelKey { - final InetSocketAddress local; - final InetSocketAddress remote; + tcpHandler = new NioTcpHandler(); } @Override @@ -292,52 +23,6 @@ public CompletableFuture sendAndReceiveTcp( Message query, byte[] data, Duration timeout) { - CompletableFuture f = new CompletableFuture<>(); - try { - final Selector selector = selector(); - long endTime = System.nanoTime() + timeout.toNanos(); - ChannelState channel = - channelMap.computeIfAbsent( - new ChannelKey(local, remote), - key -> { - log.debug("Opening async channel for l={}/r={}", local, remote); - SocketChannel c = null; - try { - c = SocketChannel.open(); - c.configureBlocking(false); - if (local != null) { - c.bind(local); - } - - c.connect(remote); - return new ChannelState(c); - } catch (IOException e) { - if (c != null) { - try { - c.close(); - } catch (IOException ee) { - // ignore - } - } - f.completeExceptionally(e); - return null; - } - }); - if (channel != null) { - log.trace( - "Creating transaction for id {} ({}/{})", - query.getHeader().getID(), - query.getQuestion().getName(), - Type.string(query.getQuestion().getType())); - Transaction t = new Transaction(query, data, endTime, channel.channel, f); - channel.pendingTransactions.add(t); - registrationQueue.add(channel); - selector.wakeup(); - } - } catch (IOException e) { - f.completeExceptionally(e); - } - - return f; + return tcpHandler.sendAndReceiveTcp(local, remote, null, query, data, timeout); } } diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java new file mode 100644 index 00000000..6641fa6f --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.EOFException; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.time.Duration; +import java.util.Iterator; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Slf4j +@Getter +public class NioTcpHandler extends NioClient { + // registrationQueue and channelMap must be static to be shared between instances + // otherwise, a second instance would overwrite the registration, timeout and close tasks of the first instance + private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); + private static final Map channelMap = new ConcurrentHashMap<>(); + + NioTcpHandler() { + setRegistrationsTask(this::processPendingRegistrations, true); + setTimeoutTask(this::checkTransactionTimeouts, true); + setCloseTask(this::closeTcp, true); + } + + private void processPendingRegistrations() { + while (!registrationQueue.isEmpty()) { + ChannelState state = registrationQueue.poll(); + if (state == null) { + continue; + } + + try { + final Selector selector = selector(); + if (!state.channel.isConnected()) { + state.channel.register(selector, SelectionKey.OP_CONNECT, state); + } else { + state.channel.keyFor(selector).interestOps(SelectionKey.OP_WRITE); + } + } catch (IOException e) { + state.handleChannelException(e); + } + } + } + + private void checkTransactionTimeouts() { + for (ChannelState state : channelMap.values()) { + for (Iterator it = state.pendingTransactions.iterator(); it.hasNext(); ) { + Transaction t = it.next(); + if (t.endTime - System.nanoTime() < 0) { + t.f.completeExceptionally(new SocketTimeoutException("Query timed out")); + it.remove(); + } + } + } + } + + private void closeTcp() { + registrationQueue.clear(); + EOFException closing = new EOFException("Client is closing"); + channelMap.forEach((key, state) -> state.handleTransactionException(closing)); + channelMap.clear(); + } + + @RequiredArgsConstructor + public static class Transaction { + private final Message query; + private final byte[] queryData; + private final long endTime; + private final SocketChannel channel; + private final CompletableFuture f; + private boolean sendDone; + + void send() throws IOException { + if (sendDone) { + return; + } + + verboseLog( + "TCP write: transaction id=" + query.getHeader().getID(), + channel.socket().getLocalSocketAddress(), + channel.socket().getRemoteSocketAddress(), + queryData); + + ByteBuffer buffer = ByteBuffer.allocate(queryData.length); + buffer.put(queryData); + buffer.flip(); + while (buffer.hasRemaining()) { + long n = channel.write(buffer); + if (n == 0) { + throw new EOFException( + "Insufficient room for the data in the underlying output buffer for transaction " + + query.getHeader().getID()); + } else if (n < queryData.length) { + throw new EOFException( + "Could not write all data for transaction " + query.getHeader().getID()); + } + } + + sendDone = true; + } + } + + @RequiredArgsConstructor + @Getter + @Setter + public class ChannelState implements KeyProcessor { + private final SocketChannel channel; + final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); + ByteBuffer responseLengthData = ByteBuffer.allocate(2); + ByteBuffer responseData = ByteBuffer.allocate(Message.MAXLENGTH); + int readState = 0; + boolean isSocks5 = false; + CompletableFuture socks5HandshakeF; + + @Override + public void processReadyKey(SelectionKey key) { + if (key.isValid()) { + if (key.isConnectable()) { + processConnect(key); + } else { + if (key.isWritable()) { + processWrite(key); + } + if (key.isReadable()) { + processRead(); + } + } + } + } + + void handleTransactionException(IOException e) { + for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { + Transaction t = it.next(); + t.f.completeExceptionally(e); + it.remove(); + } + } + + private void handleChannelException(IOException e) { + handleTransactionException(e); + for (Map.Entry entry : channelMap.entrySet()) { + if (entry.getValue() == this) { + channelMap.remove(entry.getKey()); + try { + channel.close(); + } catch (IOException ex) { + log.warn( + "Failed to close channel l={}/r={}", + entry.getKey().local, + entry.getKey().remote, + ex); + } + return; + } + } + } + + private void processConnect(SelectionKey key) { + try { + channel.finishConnect(); + key.interestOps(SelectionKey.OP_WRITE); + } catch (IOException e) { + handleChannelException(e); + } + } + + private void processRead() { + try { + if (isSocks5) { + responseData = ByteBuffer.allocate(Message.MAXLENGTH); + int read = channel.read(responseData); + if (read < 0) { + handleChannelException(new EOFException()); + return; + } + responseData.flip(); + byte[] data = new byte[responseData.limit()]; + System.arraycopy(responseData.array(), responseData.arrayOffset(), data, 0, responseData.limit()); + // the transactions for the socks5 handshake are synchronized + // you can assume that the responses are in order of the transactions in the queue + for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { + Transaction t = it.next(); + t.f.complete(data); + it.remove(); + return; + } + return; + } + + if (readState == 0) { + int read = channel.read(responseLengthData); + if (read < 0) { + handleChannelException(new EOFException()); + return; + } + + if (responseLengthData.position() == 2) { + int length = + ((responseLengthData.get(0) & 0xFF) << 8) + (responseLengthData.get(1) & 0xFF); + responseLengthData.flip(); + responseData.limit(length); + readState = 1; + } + } + + int read = channel.read(responseData); + if (read < 0) { + handleChannelException(new EOFException()); + return; + } else if (responseData.hasRemaining()) { + return; + } + } catch (IOException e) { + handleChannelException(e); + return; + } + + readState = 0; + responseData.flip(); + byte[] data = new byte[responseData.limit()]; + System.arraycopy( + responseData.array(), responseData.arrayOffset(), data, 0, responseData.limit()); + + // The message was shorter than the minimum length to find the transaction, abort + if (data.length < 2) { + verboseLog( + "TCP read: response too short for a valid reply, discarding", + channel.socket().getLocalSocketAddress(), + channel.socket().getRemoteSocketAddress(), + data); + return; + } + + int id = ((data[0] & 0xFF) << 8) + (data[1] & 0xFF); + verboseLog( + "TCP read: transaction id=" + id, + channel.socket().getLocalSocketAddress(), + channel.socket().getRemoteSocketAddress(), + data); + + for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { + Transaction t = it.next(); + int qid = t.query.getHeader().getID(); + if (id == qid) { + t.f.complete(data); + it.remove(); + return; + } + } + + log.warn("Transaction for answer to id {} not found", id); + } + + private void processWrite(SelectionKey key) { + for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { + Transaction t = it.next(); + try { + t.send(); + } catch (IOException e) { + t.f.completeExceptionally(e); + it.remove(); + } + } + + key.interestOps(SelectionKey.OP_READ); + } + + public void queueTransaction(Transaction t) { + try { + final Selector selector = selector(); + pendingTransactions.add(t); + registrationQueue.add(this); + selector.wakeup(); + } catch (IOException e) { + t.f.completeExceptionally(e); + } + } + } + + @RequiredArgsConstructor + @EqualsAndHashCode + static class ChannelKey { + final InetSocketAddress local; + final InetSocketAddress remote; + } + + public void dnsTransaction(ChannelState channel, Message query, byte[] data, long endTime, CompletableFuture f) { + // Transaction for the main data + channel.setSocks5(false); + // combine length+message to avoid multiple TCP packets + // https://datatracker.ietf.org/doc/html/rfc7766#section-8 + ByteBuffer buffer = ByteBuffer.allocate(2 + data.length); + buffer.put((byte) (data.length >>> 8)); + buffer.put((byte) (data.length & 0xFF)); + buffer.put(data); + Transaction t = new Transaction(query, buffer.array(), endTime, channel.channel, f); + channel.queueTransaction(t); + } + + public ChannelState createOrGetChannelState(InetSocketAddress local, InetSocketAddress remote, NioSocksHandler proxy, CompletableFuture f) { + return channelMap.computeIfAbsent( + new ChannelKey(local, remote), + key -> { + log.debug("Opening async channel for l={}/r={}", local, remote); + SocketChannel c = null; + try { + c = SocketChannel.open(); + c.configureBlocking(false); + if (local != null) { + c.bind(local); + } + + if (proxy != null) { + c.connect(proxy.getProxyAddress()); + } else { + c.connect(remote); + } + return new ChannelState(c); + } catch (IOException e) { + if (c != null) { + try { + c.close(); + } catch (IOException ee) { + // ignore + } + } + f.completeExceptionally(e); + return null; + } + }); + } + + public CompletableFuture sendAndReceiveTcp( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + Message query, + byte[] data, + Duration timeout) { + CompletableFuture f = new CompletableFuture<>(); + + ChannelState channel = createOrGetChannelState(local, remote, proxy, f); + if (channel != null) { + log.trace( + "Creating transaction for id {} ({}/{})", + query.getHeader().getID(), + query.getQuestion().getName(), + Type.string(query.getQuestion().getType())); + + long endTime = System.nanoTime() + timeout.toNanos(); + if (proxy != null) { + synchronized (channel) { + if (channel.socks5HandshakeF == null) { + channel.setSocks5(true); + channel.socks5HandshakeF = proxy.doSocks5Handshake(channel, NioSocksHandler.SOCKS5_CMD_CONNECT, query, endTime); + } + // Chain the SOCKS5 transactions with the main data transaction + channel.socks5HandshakeF.thenRunAsync( + () -> { + dnsTransaction(channel, query, data, endTime, f); + } + ).exceptionally(ex -> { + channel.socks5HandshakeF = null; + f.completeExceptionally(ex); + return null; + }); + } + } else { + // main DNS data transaction + dnsTransaction(channel, query, data, endTime, f); + } + } + + return f; + } + +} diff --git a/src/main/java/org/xbill/DNS/NioUdpClient.java b/src/main/java/org/xbill/DNS/NioUdpClient.java index ce6607f2..c163634a 100644 --- a/src/main/java/org/xbill/DNS/NioUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioUdpClient.java @@ -22,140 +22,10 @@ @Slf4j final class NioUdpClient extends NioClient implements UdpIoClient { - private final int ephemeralStart; - private final int ephemeralRange; - - private final SecureRandom prng; - private final Queue registrationQueue = new ConcurrentLinkedQueue<>(); - private final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); + private final NioUdpHandler udpHandler; NioUdpClient() { - // https://datatracker.ietf.org/doc/html/rfc6335#section-6 - int ephemeralStartDefault = 49152; - int ephemeralEndDefault = 65535; - - // Linux usually uses 32768-60999 - if (System.getProperty("os.name").toLowerCase().contains("linux")) { - ephemeralStartDefault = 32768; - ephemeralEndDefault = 60999; - } - - ephemeralStart = Integer.getInteger("dnsjava.udp.ephemeral.start", ephemeralStartDefault); - int ephemeralEnd = Integer.getInteger("dnsjava.udp.ephemeral.end", ephemeralEndDefault); - ephemeralRange = ephemeralEnd - ephemeralStart; - - if (Boolean.getBoolean("dnsjava.udp.ephemeral.use_ephemeral_port")) { - prng = null; - } else { - prng = new SecureRandom(); - } - setRegistrationsTask(this::processPendingRegistrations, false); - setTimeoutTask(this::checkTransactionTimeouts, false); - setCloseTask(this::closeUdp, false); - } - - private void processPendingRegistrations() { - while (!registrationQueue.isEmpty()) { - Transaction t = registrationQueue.poll(); - if (t == null) { - continue; - } - - try { - log.trace("Registering OP_READ for transaction with id {}", t.id); - t.channel.register(selector(), SelectionKey.OP_READ, t); - t.send(); - } catch (IOException e) { - t.completeExceptionally(e); - } - } - } - - private void checkTransactionTimeouts() { - for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { - Transaction t = it.next(); - if (t.endTime - System.nanoTime() < 0) { - t.completeExceptionally(new SocketTimeoutException("Query timed out")); - it.remove(); - } - } - } - - @RequiredArgsConstructor - private class Transaction implements KeyProcessor { - private final int id; - private final byte[] data; - private final int max; - private final long endTime; - private final DatagramChannel channel; - private final CompletableFuture f; - - void send() throws IOException { - ByteBuffer buffer = ByteBuffer.wrap(data); - verboseLog( - "UDP write: transaction id=" + id, - channel.socket().getLocalSocketAddress(), - channel.socket().getRemoteSocketAddress(), - data); - int n = channel.send(buffer, channel.socket().getRemoteSocketAddress()); - if (n == 0) { - throw new EOFException( - "Insufficient room for the datagram in the underlying output buffer for transaction " - + id); - } else if (n < data.length) { - throw new EOFException("Could not send all data for transaction " + id); - } - } - - @Override - public void processReadyKey(SelectionKey key) { - if (!key.isReadable()) { - completeExceptionally(new EOFException("Key for transaction " + id + " is not readable")); - pendingTransactions.remove(this); - return; - } - - DatagramChannel keyChannel = (DatagramChannel) key.channel(); - ByteBuffer buffer = ByteBuffer.allocate(max); - int read; - try { - read = keyChannel.read(buffer); - if (read <= 0) { - throw new EOFException(); - } - } catch (IOException e) { - completeExceptionally(e); - pendingTransactions.remove(this); - return; - } - - buffer.flip(); - byte[] resultingData = new byte[read]; - System.arraycopy(buffer.array(), 0, resultingData, 0, read); - verboseLog( - "UDP read: transaction id=" + id, - keyChannel.socket().getLocalSocketAddress(), - keyChannel.socket().getRemoteSocketAddress(), - resultingData); - silentDisconnectAndCloseChannel(); - f.complete(resultingData); - pendingTransactions.remove(this); - } - - private void completeExceptionally(Exception e) { - silentDisconnectAndCloseChannel(); - f.completeExceptionally(e); - } - - private void silentDisconnectAndCloseChannel() { - try { - channel.disconnect(); - } catch (IOException e) { - // ignore, we either already have everything we need or can't do anything - } finally { - NioUdpClient.silentCloseChannel(channel); - } - } + udpHandler = new NioUdpHandler(); } @Override @@ -166,77 +36,7 @@ public CompletableFuture sendAndReceiveUdp( byte[] data, int max, Duration timeout) { - long endTime = System.nanoTime() + timeout.toNanos(); - CompletableFuture f = new CompletableFuture<>(); - DatagramChannel channel = null; - try { - final Selector selector = selector(); - channel = DatagramChannel.open(); - channel.configureBlocking(false); - - Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, channel, f); - if (local == null || local.getPort() == 0) { - boolean bound = false; - for (int i = 0; i < 1024; i++) { - try { - InetSocketAddress addr = null; - if (local == null) { - if (prng != null) { - addr = new InetSocketAddress(prng.nextInt(ephemeralRange) + ephemeralStart); - } - } else { - int port = local.getPort(); - if (port == 0 && prng != null) { - port = prng.nextInt(ephemeralRange) + ephemeralStart; - } - - addr = new InetSocketAddress(local.getAddress(), port); - } - - channel.bind(addr); - bound = true; - break; - } catch (SocketException e) { - // ignore, we'll try another random port - } - } - - if (!bound) { - t.completeExceptionally(new IOException("No available source port found")); - return f; - } - } - - channel.connect(remote); - pendingTransactions.add(t); - registrationQueue.add(t); - selector.wakeup(); - } catch (IOException e) { - silentCloseChannel(channel); - f.completeExceptionally(e); - } catch (Throwable e) { - // Make sure to close the channel, no matter what, but only handle the declared IOException - silentCloseChannel(channel); - throw e; - } - - return f; + return udpHandler.sendAndReceiveUdp(local, remote, null, query, data, max, timeout); } - private static void silentCloseChannel(DatagramChannel channel) { - if (channel != null) { - try { - channel.close(); - } catch (IOException ioe) { - // ignore - } - } - } - - private void closeUdp() { - registrationQueue.clear(); - EOFException closing = new EOFException("Client is closing"); - pendingTransactions.forEach(t -> t.completeExceptionally(closing)); - pendingTransactions.clear(); - } } diff --git a/src/main/java/org/xbill/DNS/NioUdpHandler.java b/src/main/java/org/xbill/DNS/NioUdpHandler.java new file mode 100644 index 00000000..9461d19a --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioUdpHandler.java @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.EOFException; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Slf4j +final class NioUdpHandler extends NioClient { + private final int ephemeralStart; + private final int ephemeralRange; + + private final SecureRandom prng; + private final Queue registrationQueue = new ConcurrentLinkedQueue<>(); + private final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); + + NioUdpHandler() { + // https://datatracker.ietf.org/doc/html/rfc6335#section-6 + int ephemeralStartDefault = 49152; + int ephemeralEndDefault = 65535; + + // Linux usually uses 32768-60999 + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + ephemeralStartDefault = 32768; + ephemeralEndDefault = 60999; + } + + ephemeralStart = Integer.getInteger("dnsjava.udp.ephemeral.start", ephemeralStartDefault); + int ephemeralEnd = Integer.getInteger("dnsjava.udp.ephemeral.end", ephemeralEndDefault); + ephemeralRange = ephemeralEnd - ephemeralStart; + + if (Boolean.getBoolean("dnsjava.udp.ephemeral.use_ephemeral_port")) { + prng = null; + } else { + prng = new SecureRandom(); + } + setRegistrationsTask(this::processPendingRegistrations, false); + setTimeoutTask(this::checkTransactionTimeouts, false); + setCloseTask(this::closeUdp, false); + } + + private void processPendingRegistrations() { + while (!registrationQueue.isEmpty()) { + Transaction t = registrationQueue.poll(); + if (t == null) { + continue; + } + + try { + log.trace("Registering OP_READ for transaction with id {}", t.id); + t.channel.register(selector(), SelectionKey.OP_READ, t); + t.send(); + } catch (IOException e) { + t.completeExceptionally(e); + } + } + } + + private void checkTransactionTimeouts() { + for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { + Transaction t = it.next(); + if (t.endTime - System.nanoTime() < 0) { + t.completeExceptionally(new SocketTimeoutException("Query timed out")); + it.remove(); + } + } + } + + @RequiredArgsConstructor + private class Transaction implements KeyProcessor { + private final int id; + private final byte[] data; + private final int max; + private final long endTime; + private final DatagramChannel channel; + private final boolean isProxyChannel; + private final CompletableFuture f; + + void send() throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(data); + verboseLog( + "UDP write: transaction id=" + id, + channel.socket().getLocalSocketAddress(), + channel.socket().getRemoteSocketAddress(), + data); + int n = channel.send(buffer, channel.socket().getRemoteSocketAddress()); + if (n == 0) { + throw new EOFException( + "Insufficient room for the datagram in the underlying output buffer for transaction " + + id); + } else if (n < data.length) { + throw new EOFException("Could not send all data for transaction " + id); + } + } + + @Override + public void processReadyKey(SelectionKey key) { + if (!key.isReadable()) { + completeExceptionally(new EOFException("Key for transaction " + id + " is not readable")); + pendingTransactions.remove(this); + return; + } + + DatagramChannel keyChannel = (DatagramChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(max); + int read; + try { + read = keyChannel.read(buffer); + if (read <= 0) { + throw new EOFException(); + } + } catch (IOException e) { + completeExceptionally(e); + pendingTransactions.remove(this); + return; + } + + buffer.flip(); + byte[] resultingData = new byte[read]; + System.arraycopy(buffer.array(), 0, resultingData, 0, read); + verboseLog( + "UDP read: transaction id=" + id, + keyChannel.socket().getLocalSocketAddress(), + keyChannel.socket().getRemoteSocketAddress(), + resultingData); + // do not close the channel in case of SOCKS5 UDP associate. + // the channel port needs to be claimed for further queries to the same remote host. + // you can not use the same UDP associate port with another local port after the first query. + // you can also close this channel and open a new one with the same local port for further queries, + // but I would like to avoid, that the local port will be taken by another process between queries. + if (!isProxyChannel) { + silentDisconnectAndCloseChannel(); + } + f.complete(resultingData); + pendingTransactions.remove(this); + } + + private void completeExceptionally(Exception e) { + silentDisconnectAndCloseChannel(); + f.completeExceptionally(e); + } + + private void silentDisconnectAndCloseChannel() { + try { + channel.disconnect(); + } catch (IOException e) { + // ignore, we either already have everything we need or can't do anything + } finally { + NioUdpHandler.silentCloseChannel(channel); + } + } + } + + public DatagramChannel createChannel(InetSocketAddress local, InetSocketAddress remote, CompletableFuture f) throws IOException { + DatagramChannel channel = DatagramChannel.open(); + channel.configureBlocking(false); + if (local == null || local.getPort() == 0) { + boolean bound = false; + for (int i = 0; i < 1024; i++) { + try { + InetSocketAddress addr = null; + if (local == null) { + if (prng != null) { + addr = new InetSocketAddress(prng.nextInt(ephemeralRange) + ephemeralStart); + } + } else { + int port = local.getPort(); + if (port == 0 && prng != null) { + port = prng.nextInt(ephemeralRange) + ephemeralStart; + } + + addr = new InetSocketAddress(local.getAddress(), port); + } + + channel.bind(addr); + bound = true; + break; + } catch (SocketException e) { + // ignore, we'll try another random port + } + } + if (!bound) { + f.completeExceptionally(new IOException("No available source port found")); + return null; + } + } else { + channel.bind(local); + } + return channel.connect(remote); + } + + public CompletableFuture sendAndReceiveUdp( + InetSocketAddress local, + InetSocketAddress remote, + DatagramChannel channel, + Message query, + byte[] data, + int max, + Duration timeout) { + long endTime = System.nanoTime() + timeout.toNanos(); + CompletableFuture f = new CompletableFuture<>(); + + try { + boolean isProxyChannel = false; + if (channel == null) { + channel = createChannel(local, remote, f); + } else { + // Do not close the channel in case of SOCKS5 UDP associate. + // close it only on Exception and complete the future exceptionally. + isProxyChannel = true; + } + + Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, channel, isProxyChannel, f); + + final Selector selector = selector(); + pendingTransactions.add(t); + registrationQueue.add(t); + selector.wakeup(); + } catch (IOException e) { + silentCloseChannel(channel); + f.completeExceptionally(e); + } catch (Throwable e) { + // Make sure to close the channel, no matter what, but only handle the declared IOException + silentCloseChannel(channel); + throw e; + } + + return f; + } + + private static void silentCloseChannel(DatagramChannel channel) { + if (channel != null) { + try { + channel.close(); + } catch (IOException ioe) { + // ignore + } + } + } + + private void closeUdp() { + registrationQueue.clear(); + EOFException closing = new EOFException("Client is closing"); + pendingTransactions.forEach(t -> t.completeExceptionally(closing)); + pendingTransactions.clear(); + } +} diff --git a/src/main/java/org/xbill/DNS/Socks5Proxy.java b/src/main/java/org/xbill/DNS/Socks5Proxy.java index fe7ab9fe..0899b0a0 100644 --- a/src/main/java/org/xbill/DNS/Socks5Proxy.java +++ b/src/main/java/org/xbill/DNS/Socks5Proxy.java @@ -59,8 +59,9 @@ public enum Command { } private final Socks5ProxyConfig config; - private final InetSocketAddress local; - private final InetSocketAddress remote; + private final InetSocketAddress localAddress; + private final InetSocketAddress proxyAddress; + private final InetSocketAddress remoteAddress; private SelectionKey tcpSelectionKey; private DatagramChannel udpChannel; private final Command command; @@ -70,12 +71,14 @@ public Socks5Proxy( SelectionKey tcpSelectionKey, Socks5ProxyConfig config, InetSocketAddress local, + InetSocketAddress proxyAddress, InetSocketAddress remote, Command command) { this.tcpSelectionKey = tcpSelectionKey; this.config = config; - this.local = local; - this.remote = remote; + this.localAddress = local; + this.proxyAddress = proxyAddress; + this.remoteAddress = remote; this.command = command; } @@ -89,17 +92,17 @@ public ByteBuffer addSocks5UdpAssociateHeader(byte[] data) { ByteBuffer buffer; byte addressType; byte[] addressBytes; - if (remote.getAddress() instanceof Inet4Address) { + if (remoteAddress.getAddress() instanceof Inet4Address) { addressType = SOCKS5_ATYP_IPV4; - addressBytes = remote.getAddress().getAddress(); + addressBytes = remoteAddress.getAddress().getAddress(); buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); - } else if (remote.getAddress() instanceof Inet6Address) { + } else if (remoteAddress.getAddress() instanceof Inet6Address) { addressType = SOCKS5_ATYP_IPV6; - addressBytes = remote.getAddress().getAddress(); + addressBytes = remoteAddress.getAddress().getAddress(); buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); } else { addressType = SOCKS5_ATYP_DOMAINNAME; - addressBytes = remote.getHostName().getBytes(StandardCharsets.UTF_8); + addressBytes = remoteAddress.getHostName().getBytes(StandardCharsets.UTF_8); buffer = ByteBuffer.allocate(4 + 1 + addressBytes.length + 2 + data.length); } @@ -111,7 +114,7 @@ public ByteBuffer addSocks5UdpAssociateHeader(byte[] data) { buffer.put((byte) addressBytes.length); } buffer.put(addressBytes); // DST.ADDR - buffer.putShort((short) remote.getPort()); // DST.PORT + buffer.putShort((short) remoteAddress.getPort()); // DST.PORT buffer.put(data); // DATA return buffer; @@ -297,17 +300,17 @@ public void run() { byte addressType; byte[] addressBytes; - if (remote.getAddress() instanceof Inet4Address) { + if (remoteAddress.getAddress() instanceof Inet4Address) { addressType = SOCKS5_ATYP_IPV4; - addressBytes = remote.getAddress().getAddress(); + addressBytes = remoteAddress.getAddress().getAddress(); buffer = ByteBuffer.allocate(10); - } else if (remote.getAddress() instanceof Inet6Address) { + } else if (remoteAddress.getAddress() instanceof Inet6Address) { addressType = SOCKS5_ATYP_IPV6; - addressBytes = remote.getAddress().getAddress(); + addressBytes = remoteAddress.getAddress().getAddress(); buffer = ByteBuffer.allocate(22); } else { addressType = SOCKS5_ATYP_DOMAINNAME; - addressBytes = remote.getHostName().getBytes(StandardCharsets.UTF_8); + addressBytes = remoteAddress.getHostName().getBytes(StandardCharsets.UTF_8); buffer = ByteBuffer.allocate(7 + addressBytes.length); } @@ -319,7 +322,7 @@ public void run() { buffer.put((byte) addressBytes.length); } buffer.put(addressBytes); - buffer.putShort((short) remote.getPort()); + buffer.putShort((short) remoteAddress.getPort()); buffer.flip(); channel.write(buffer); @@ -416,7 +419,7 @@ public void run() { int udpAssociatePort = Short.toUnsignedInt(buffer.getShort()); udpChannel = DatagramChannel.open(); udpChannel.configureBlocking(false); - udpChannel.bind(new InetSocketAddress(local.getAddress(), 0)); + udpChannel.bind(new InetSocketAddress(localAddress.getAddress(), 0)); udpChannel.connect(new InetSocketAddress(config.getProxyAddress().getAddress(), udpAssociatePort)); } diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java b/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java index 6284e43a..b871cc41 100644 --- a/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java +++ b/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java @@ -1,236 +1,236 @@ -package org.xbill.DNS; - -import java.io.IOException; -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.xbill.DNS.io.IoClientFactory; -import org.xbill.DNS.io.TcpIoClient; -import org.xbill.DNS.io.UdpIoClient; - -@Getter -@Slf4j -public class Socks5ProxyIoClientFactory implements IoClientFactory { - - // SOCKS5 proxy configuration - private final Socks5ProxyConfig config; - - // connection pool Socks5ProxyConnection - private static final Map> socks5ConnectionPool = new ConcurrentHashMap<>(); - - // selector for handling IO events - private volatile Selector selector; - private Thread eventLoopThread; - private volatile boolean eventLoopRunning = false; - - // scheduler for handling timeouts, cleanup and closing connections - private ScheduledExecutorService timeoutScheduler; - private static final long timeout = 30000; // 30 seconds timeout - private final Map keyTimestamps = new ConcurrentHashMap<>(); - - // constructor - public Socks5ProxyIoClientFactory(Socks5ProxyConfig socks5Proxy) { - config = Objects.requireNonNull(socks5Proxy, "proxy config must not be null"); - - // start event loop if not already running - startEventLoop(); - - // Add shutdown hook for graceful shutdown - Runtime.getRuntime().addShutdownHook(new Thread(this::stopEventLoop)); - } - - // method to start the event loop - private synchronized void startEventLoop() { - try { - selector = Selector.open(); - } catch (IOException e) { - log.error("Error opening selector", e); - return; - } - - eventLoopRunning = true; - timeoutScheduler = Executors.newScheduledThreadPool(1); - eventLoopThread = new Thread(() -> { - try { - while (eventLoopRunning) { - // blocking call, waits for an io event - selector.select(); - - // get the set of keys with pending events - Set selectedKeys = selector.selectedKeys(); - Iterator keyIterator = selectedKeys.iterator(); - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - keyIterator.remove(); - if (key.isValid()) { - // run the task associated with the key - ((Runnable) key.attachment()).run(); - // update the timestamp for the key - long currentTime = System.currentTimeMillis(); - keyTimestamps.put(key, currentTime); - scheduleTimeout(selector, key); - } - } - } - } catch (IOException e) { - log.error("Error in event loop", e); - } finally { - try { - if (selector != null) { - selector.close(); - } - } catch (IOException e) { - log.error("Error closing selector", e); - } - eventLoopRunning = false; - } - }); - eventLoopThread.start(); - } - - private void scheduleTimeout(Selector selector, SelectionKey key) { - timeoutScheduler.schedule(() -> { - long currentTime = System.currentTimeMillis(); - if (currentTime - keyTimestamps.getOrDefault(key, 0L) > timeout) { - log.debug("Closing connection due to timeout"); - try { - key.cancel(); - key.channel().close(); - } catch (IOException e) { - log.error("Error closing channel due to timeout", e); - } - keyTimestamps.remove(key); - selector.wakeup(); - } - }, timeout, TimeUnit.MILLISECONDS); - } - - // graceful shutdown of the event loop - public void stopEventLoop() { - // stop the event loop - eventLoopRunning = false; - if (eventLoopThread != null) { - eventLoopThread.interrupt(); - } - // stop the timeout scheduler - if (timeoutScheduler != null) { - timeoutScheduler.shutdownNow(); - } - // close all connections in the pool - for (Map subConnections : socks5ConnectionPool.values()) { - for (Socks5Proxy connection : subConnections.values()) { - try { - connection.getTcpSelectionKey().channel().close(); - connection.getTcpSelectionKey().cancel(); - } catch (IOException e) { - log.error("Error closing connection", e); - } - } - } - // close the selector - try { - selector.close(); - } catch (IOException e) { - log.error("Error closing selector", e); - } - socks5ConnectionPool.clear(); - } - - // check if the event loop thread is alive for health checks - public boolean isEventLoopThreadAlive() { - return eventLoopThread != null && eventLoopThread.isAlive(); - } - - // check if the timeout scheduler is running for health checks - public boolean isTimeoutSchedulerRunning() { - return timeoutScheduler != null && !timeoutScheduler.isShutdown(); - } - - // check if the event loop is healthy overall - public boolean isEventLoopHealthy() { - return isEventLoopThreadAlive() && isTimeoutSchedulerRunning(); - } - - // register a new connection to the selector - public SelectionKey registerToSelector(SelectableChannel conn) throws IOException { - return conn.register(selector, SelectionKey.OP_CONNECT); - } - - // unregister a connection from the selector - public synchronized void unregisterFromSelector( - PoolConn poolConn, - Throwable ex) throws IOException { - if ( - ex == null - && poolConn.getSocks5Conn().getTcpSelectionKey().isValid() - && poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen() - ) { - // unregister for reuse - poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); - } else { - // clean up the socks connection instance in case of an exception or invalid state - cleanupConnectionFromPool(poolConn); - } - } - - public synchronized PoolConn getPoolConnFromPool(String connectionID) { - Map subConnections = socks5ConnectionPool.get(connectionID); - if (subConnections != null && !subConnections.isEmpty()) { - for (Map.Entry entry : subConnections.entrySet()) { - Socks5Proxy socks5Conn = entry.getValue(); - if (socks5Conn.getTcpSelectionKey().channel().isOpen() - && !socks5Conn.getTcpSelectionKey().channel().isRegistered()) { - return new PoolConn(connectionID, entry.getKey(), socks5Conn); - } - } - } - return null; - } - - @Getter - @RequiredArgsConstructor - public static class PoolConn { - private final String connectionID; - private final String subConnectionID; - private final Socks5Proxy socks5Conn; - } - - public synchronized PoolConn addConnectionToPool(String connectionID, Socks5Proxy socks5Conn) { - String subConnectionID = UUID.randomUUID().toString(); - socks5ConnectionPool.computeIfAbsent(connectionID, k -> new ConcurrentHashMap<>()).put(subConnectionID, socks5Conn); - return new PoolConn(connectionID, subConnectionID, socks5Conn); - } - - public synchronized void cleanupConnectionFromPool(PoolConn poolConn) throws IOException { - if (poolConn.getSocks5Conn() != null && poolConn.getSocks5Conn().getTcpSelectionKey() != null) { - poolConn.getSocks5Conn().getTcpSelectionKey().channel().close(); - poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); - } - Map subConnections = socks5ConnectionPool.get(poolConn.getConnectionID()); - if (subConnections != null) { - subConnections.remove(poolConn.getSubConnectionID()); - if (subConnections.isEmpty()) { - socks5ConnectionPool.remove(poolConn.getConnectionID()); - } - } - } - - @Override - public TcpIoClient createOrGetTcpClient() { - return new Socks5ProxyTcpIoClient(this, config); - } - - @Override - public UdpIoClient createOrGetUdpClient() { - return new Socks5ProxyUdpIoClient(this, config); - } -} +//package org.xbill.DNS; +// +//import java.io.IOException; +//import java.nio.channels.SelectableChannel; +//import java.nio.channels.SelectionKey; +//import java.nio.channels.Selector; +//import java.util.*; +//import java.util.concurrent.ConcurrentHashMap; +//import java.util.concurrent.Executors; +//import java.util.concurrent.ScheduledExecutorService; +//import java.util.concurrent.TimeUnit; +// +//import lombok.Getter; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.xbill.DNS.io.IoClientFactory; +//import org.xbill.DNS.io.TcpIoClient; +//import org.xbill.DNS.io.UdpIoClient; +// +//@Getter +//@Slf4j +//public class Socks5ProxyIoClientFactory implements IoClientFactory { +// +// // SOCKS5 proxy configuration +// private final Socks5ProxyConfig config; +// +// // connection pool Socks5ProxyConnection +// private static final Map> socks5ConnectionPool = new ConcurrentHashMap<>(); +// +// // selector for handling IO events +// private volatile Selector selector; +// private Thread eventLoopThread; +// private volatile boolean eventLoopRunning = false; +// +// // scheduler for handling timeouts, cleanup and closing connections +// private ScheduledExecutorService timeoutScheduler; +// private static final long timeout = 30000; // 30 seconds timeout +// private final Map keyTimestamps = new ConcurrentHashMap<>(); +// +// // constructor +// public Socks5ProxyIoClientFactory(Socks5ProxyConfig socks5Proxy) { +// config = Objects.requireNonNull(socks5Proxy, "proxy config must not be null"); +// +// // start event loop if not already running +// startEventLoop(); +// +// // Add shutdown hook for graceful shutdown +// Runtime.getRuntime().addShutdownHook(new Thread(this::stopEventLoop)); +// } +// +// // method to start the event loop +// private synchronized void startEventLoop() { +// try { +// selector = Selector.open(); +// } catch (IOException e) { +// log.error("Error opening selector", e); +// return; +// } +// +// eventLoopRunning = true; +// timeoutScheduler = Executors.newScheduledThreadPool(1); +// eventLoopThread = new Thread(() -> { +// try { +// while (eventLoopRunning) { +// // blocking call, waits for an io event +// selector.select(); +// +// // get the set of keys with pending events +// Set selectedKeys = selector.selectedKeys(); +// Iterator keyIterator = selectedKeys.iterator(); +// while (keyIterator.hasNext()) { +// SelectionKey key = keyIterator.next(); +// keyIterator.remove(); +// if (key.isValid()) { +// // run the task associated with the key +// ((Runnable) key.attachment()).run(); +// // update the timestamp for the key +//// long currentTime = System.currentTimeMillis(); +//// keyTimestamps.put(key, currentTime); +//// scheduleTimeout(selector, key); +// } +// } +// } +// } catch (IOException e) { +// log.error("Error in event loop", e); +// } finally { +// try { +// if (selector != null) { +// selector.close(); +// } +// } catch (IOException e) { +// log.error("Error closing selector", e); +// } +// eventLoopRunning = false; +// } +// }); +// eventLoopThread.start(); +// } +// +// private void scheduleTimeout(Selector selector, SelectionKey key) { +// timeoutScheduler.schedule(() -> { +// long currentTime = System.currentTimeMillis(); +// if (currentTime - keyTimestamps.getOrDefault(key, 0L) > timeout) { +// log.debug("Closing connection due to timeout"); +// try { +// key.cancel(); +// key.channel().close(); +// } catch (IOException e) { +// log.error("Error closing channel due to timeout", e); +// } +// keyTimestamps.remove(key); +// selector.wakeup(); +// } +// }, timeout, TimeUnit.MILLISECONDS); +// } +// +// // graceful shutdown of the event loop +// public void stopEventLoop() { +// // stop the event loop +// eventLoopRunning = false; +// if (eventLoopThread != null) { +// eventLoopThread.interrupt(); +// } +// // stop the timeout scheduler +// if (timeoutScheduler != null) { +// timeoutScheduler.shutdownNow(); +// } +// // close all connections in the pool +// for (Map subConnections : socks5ConnectionPool.values()) { +// for (Socks5Proxy connection : subConnections.values()) { +// try { +// connection.getTcpSelectionKey().channel().close(); +// connection.getTcpSelectionKey().cancel(); +// } catch (IOException e) { +// log.error("Error closing connection", e); +// } +// } +// } +// // close the selector +// try { +// selector.close(); +// } catch (IOException e) { +// log.error("Error closing selector", e); +// } +// socks5ConnectionPool.clear(); +// } +// +// // check if the event loop thread is alive for health checks +// public boolean isEventLoopThreadAlive() { +// return eventLoopThread != null && eventLoopThread.isAlive(); +// } +// +// // check if the timeout scheduler is running for health checks +// public boolean isTimeoutSchedulerRunning() { +// return timeoutScheduler != null && !timeoutScheduler.isShutdown(); +// } +// +// // check if the event loop is healthy overall +// public boolean isEventLoopHealthy() { +// return isEventLoopThreadAlive() && isTimeoutSchedulerRunning(); +// } +// +// // register a new connection to the selector +// public SelectionKey registerToSelector(SelectableChannel conn) throws IOException { +// return conn.register(selector, SelectionKey.OP_CONNECT); +// } +// +// // unregister a connection from the selector +// public synchronized void unregisterFromSelector( +// PoolConn poolConn, +// Throwable ex) throws IOException { +// if ( +// ex == null +// && poolConn.getSocks5Conn().getTcpSelectionKey().isValid() +// && poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen() +// ) { +// // unregister for reuse +// poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); +// } else { +// // clean up the socks connection instance in case of an exception or invalid state +// cleanupConnectionFromPool(poolConn); +// } +// } +// +// public synchronized PoolConn getPoolConnFromPool(String connectionID) { +// Map subConnections = socks5ConnectionPool.get(connectionID); +// if (subConnections != null && !subConnections.isEmpty()) { +// for (Map.Entry entry : subConnections.entrySet()) { +// Socks5Proxy socks5Conn = entry.getValue(); +// if (socks5Conn.getTcpSelectionKey().channel().isOpen() +// && !socks5Conn.getTcpSelectionKey().channel().isRegistered()) { +// return new PoolConn(connectionID, entry.getKey(), socks5Conn); +// } +// } +// } +// return null; +// } +// +// @Getter +// @RequiredArgsConstructor +// public static class PoolConn { +// private final String connectionID; +// private final String subConnectionID; +// private final Socks5Proxy socks5Conn; +// } +// +// public synchronized PoolConn addConnectionToPool(String connectionID, Socks5Proxy socks5Conn) { +// String subConnectionID = UUID.randomUUID().toString(); +// socks5ConnectionPool.computeIfAbsent(connectionID, k -> new ConcurrentHashMap<>()).put(subConnectionID, socks5Conn); +// return new PoolConn(connectionID, subConnectionID, socks5Conn); +// } +// +// public synchronized void cleanupConnectionFromPool(PoolConn poolConn) throws IOException { +// if (poolConn.getSocks5Conn() != null && poolConn.getSocks5Conn().getTcpSelectionKey() != null) { +// poolConn.getSocks5Conn().getTcpSelectionKey().channel().close(); +// poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); +// } +// Map subConnections = socks5ConnectionPool.get(poolConn.getConnectionID()); +// if (subConnections != null) { +// subConnections.remove(poolConn.getSubConnectionID()); +// if (subConnections.isEmpty()) { +// socks5ConnectionPool.remove(poolConn.getConnectionID()); +// } +// } +// } +// +// @Override +// public TcpIoClient createOrGetTcpClient() { +// return new Socks5ProxyTcpIoClient(this, config); +// } +// +// @Override +// public UdpIoClient createOrGetUdpClient() { +// return new Socks5ProxyUdpIoClient(this, config); +// } +//} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java b/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java index 698e4abc..f81fd5d4 100644 --- a/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java +++ b/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java @@ -1,156 +1,156 @@ -package org.xbill.DNS; - -import lombok.extern.slf4j.Slf4j; -import org.xbill.DNS.io.TcpIoClient; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; - -@Slf4j -public class Socks5ProxyTcpIoClient implements TcpIoClient { - private final Socks5ProxyIoClientFactory factory; - private final Socks5ProxyConfig config; - private Socks5ProxyIoClientFactory.PoolConn poolConn; - - public Socks5ProxyTcpIoClient(Socks5ProxyIoClientFactory factory, Socks5ProxyConfig config) { - this.factory = factory; - this.config = config; - } - - public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { - Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); - try { - if (poolConn == null || !poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen()) { - SocketChannel tcpConn = SocketChannel.open(); - tcpConn.configureBlocking(false); - SelectionKey selectionKey = factory.registerToSelector(tcpConn); - Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.CONNECT); - tcpConn.connect(config.getProxyAddress()); - socks5Conn.handleSOCKS5(f); - this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); - } else { - SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); - poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); - this.poolConn = poolConn; - f.complete(null); - } - } catch (IOException e) { - f.completeExceptionally(e); - } - } - - @Override - public CompletableFuture sendAndReceiveTcp( - InetSocketAddress local, - InetSocketAddress remote, - Message query, - byte[] data, - Duration timeout) { - if (local == null) { - local = new InetSocketAddress(0); - } - // keyString is used to identify and reuse SOCKS5 connections - String keyString = local.toString() + "-" + remote.toString() + "-TCP"; - CompletableFuture socksF = new CompletableFuture<>(); - this.initOrReuseConn(socksF, keyString, local, remote); - - return socksF.thenComposeAsync(v -> { - CompletableFuture dataF = new CompletableFuture<>(); - try { - poolConn.getSocks5Conn().getTcpSelectionKey().attach(new SendHandler(dataF, data, poolConn.getSocks5Conn().getTcpSelectionKey())); - poolConn.getSocks5Conn().getTcpSelectionKey().interestOps(SelectionKey.OP_WRITE); - poolConn.getSocks5Conn().getTcpSelectionKey().selector().wakeup(); - } catch (Exception e) { - dataF.completeExceptionally(e); - } - return dataF; - }).whenComplete((result, ex) -> { - try { - factory.unregisterFromSelector(poolConn, ex); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - private class SendHandler implements Runnable { - private final CompletableFuture future; - private final byte[] data; - private final SelectionKey selectionKey; - - public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { - this.future = future; - this.data = data; - this.selectionKey = selectionKey; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) selectionKey.channel(); - ByteBuffer buffer = ByteBuffer.allocate(data.length + 2); - buffer.put((byte) (data.length >>> 8)); - buffer.put((byte) (data.length & 0xFF)); - buffer.put(data); - buffer.flip(); - while (buffer.hasRemaining()) { - channel.write(buffer); - } - selectionKey.attach(new ReceiveHandler(future, selectionKey)); - selectionKey.interestOps(SelectionKey.OP_READ); - selectionKey.selector().wakeup(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } - - private class ReceiveHandler implements Runnable { - private final CompletableFuture future; - private final SelectionKey selectionKey; - private final ByteBuffer responseLengthData = ByteBuffer.allocate(2); - private ByteBuffer responseData; - - public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey) { - this.future = future; - this.selectionKey = selectionKey; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) selectionKey.channel(); - if (responseData == null) { - int read = channel.read(responseLengthData); - if (read < 0) { - throw new IOException("Connection closed by peer"); - } - if (responseLengthData.position() == 2) { - responseLengthData.flip(); - int length = ((responseLengthData.get(0) & 0xFF) << 8) + (responseLengthData.get(1) & 0xFF); - responseData = ByteBuffer.allocate(length); - } - } - if (responseData != null) { - int read = channel.read(responseData); - if (read < 0) { - throw new IOException("Connection closed by peer"); - } - if (!responseData.hasRemaining()) { - responseData.flip(); - byte[] data = new byte[responseData.limit()]; - responseData.get(data); - future.complete(data); - } - } - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } -} +//package org.xbill.DNS; +// +//import lombok.extern.slf4j.Slf4j; +//import org.xbill.DNS.io.TcpIoClient; +// +//import java.io.IOException; +//import java.net.InetSocketAddress; +//import java.nio.ByteBuffer; +//import java.nio.channels.SelectionKey; +//import java.nio.channels.SocketChannel; +//import java.time.Duration; +//import java.util.concurrent.CompletableFuture; +// +//@Slf4j +//public class Socks5ProxyTcpIoClient implements TcpIoClient { +// private final Socks5ProxyIoClientFactory factory; +// private final Socks5ProxyConfig config; +// private Socks5ProxyIoClientFactory.PoolConn poolConn; +// +// public Socks5ProxyTcpIoClient(Socks5ProxyIoClientFactory factory, Socks5ProxyConfig config) { +// this.factory = factory; +// this.config = config; +// } +// +// public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { +// Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); +// try { +// if (poolConn == null || !poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen()) { +// SocketChannel tcpConn = SocketChannel.open(); +// tcpConn.configureBlocking(false); +// SelectionKey selectionKey = factory.registerToSelector(tcpConn); +// Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.CONNECT); +// tcpConn.connect(config.getProxyAddress()); +// socks5Conn.handleSOCKS5(f); +// this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); +// } else { +// SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); +// poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); +// this.poolConn = poolConn; +// f.complete(null); +// } +// } catch (IOException e) { +// f.completeExceptionally(e); +// } +// } +// +// @Override +// public CompletableFuture sendAndReceiveTcp( +// InetSocketAddress local, +// InetSocketAddress remote, +// Message query, +// byte[] data, +// Duration timeout) { +// if (local == null) { +// local = new InetSocketAddress(0); +// } +// // keyString is used to identify and reuse SOCKS5 connections +// String keyString = local.toString() + "-" + remote.toString() + "-TCP"; +// CompletableFuture socksF = new CompletableFuture<>(); +// this.initOrReuseConn(socksF, keyString, local, remote); +// +// return socksF.thenComposeAsync(v -> { +// CompletableFuture dataF = new CompletableFuture<>(); +// try { +// poolConn.getSocks5Conn().getTcpSelectionKey().attach(new SendHandler(dataF, data, poolConn.getSocks5Conn().getTcpSelectionKey())); +// poolConn.getSocks5Conn().getTcpSelectionKey().interestOps(SelectionKey.OP_WRITE); +// poolConn.getSocks5Conn().getTcpSelectionKey().selector().wakeup(); +// } catch (Exception e) { +// dataF.completeExceptionally(e); +// } +// return dataF; +// }).whenComplete((result, ex) -> { +// try { +// factory.unregisterFromSelector(poolConn, ex); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } +// }); +// } +// +// private class SendHandler implements Runnable { +// private final CompletableFuture future; +// private final byte[] data; +// private final SelectionKey selectionKey; +// +// public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { +// this.future = future; +// this.data = data; +// this.selectionKey = selectionKey; +// } +// +// @Override +// public void run() { +// try { +// SocketChannel channel = (SocketChannel) selectionKey.channel(); +// ByteBuffer buffer = ByteBuffer.allocate(data.length + 2); +// buffer.put((byte) (data.length >>> 8)); +// buffer.put((byte) (data.length & 0xFF)); +// buffer.put(data); +// buffer.flip(); +// while (buffer.hasRemaining()) { +// channel.write(buffer); +// } +// selectionKey.attach(new ReceiveHandler(future, selectionKey)); +// selectionKey.interestOps(SelectionKey.OP_READ); +// selectionKey.selector().wakeup(); +// } catch (IOException e) { +// future.completeExceptionally(e); +// } +// } +// } +// +// private class ReceiveHandler implements Runnable { +// private final CompletableFuture future; +// private final SelectionKey selectionKey; +// private final ByteBuffer responseLengthData = ByteBuffer.allocate(2); +// private ByteBuffer responseData; +// +// public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey) { +// this.future = future; +// this.selectionKey = selectionKey; +// } +// +// @Override +// public void run() { +// try { +// SocketChannel channel = (SocketChannel) selectionKey.channel(); +// if (responseData == null) { +// int read = channel.read(responseLengthData); +// if (read < 0) { +// throw new IOException("Connection closed by peer"); +// } +// if (responseLengthData.position() == 2) { +// responseLengthData.flip(); +// int length = ((responseLengthData.get(0) & 0xFF) << 8) + (responseLengthData.get(1) & 0xFF); +// responseData = ByteBuffer.allocate(length); +// } +// } +// if (responseData != null) { +// int read = channel.read(responseData); +// if (read < 0) { +// throw new IOException("Connection closed by peer"); +// } +// if (!responseData.hasRemaining()) { +// responseData.flip(); +// byte[] data = new byte[responseData.limit()]; +// responseData.get(data); +// future.complete(data); +// } +// } +// } catch (IOException e) { +// future.completeExceptionally(e); +// } +// } +// } +//} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java b/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java index b8243074..a692ec5a 100644 --- a/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java +++ b/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java @@ -1,158 +1,158 @@ -package org.xbill.DNS; - -import org.xbill.DNS.io.UdpIoClient; - -import java.io.EOFException; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; - -public class Socks5ProxyUdpIoClient implements UdpIoClient { - private final Socks5ProxyIoClientFactory factory; - private Socks5ProxyIoClientFactory.PoolConn poolConn; - private SelectionKey udpSelectionKey; - private final Socks5ProxyConfig config; - private int max; - - public Socks5ProxyUdpIoClient( - Socks5ProxyIoClientFactory factory, - Socks5ProxyConfig config) { - this.factory = factory; - this.config = config; - } - - public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { - Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); - try { - if (poolConn == null) { - SocketChannel tcpConn = SocketChannel.open(); - tcpConn.configureBlocking(false); - SelectionKey selectionKey = factory.registerToSelector(tcpConn); - Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.UDP_ASSOCIATE); - tcpConn.connect(config.getProxyAddress()); - socks5Conn.handleSOCKS5(f); - this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); - } else { - SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); - poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); - this.poolConn = poolConn; - f.complete(null); - } - } catch (IOException e) { - f.completeExceptionally(e); - } - } - - - @Override - public CompletableFuture sendAndReceiveUdp( - InetSocketAddress local, - InetSocketAddress remote, - Message query, - byte[] data, - int max, - Duration timeout) { - this.max = max; - InetSocketAddress finalLocal; - if (local == null) { - finalLocal = new InetSocketAddress(0); - } else { - finalLocal = local; - } - // keyString is used to identify and reuse SOCKS5 connections - String keyString = finalLocal.toString() + "-" + remote.toString() + "-UDP"; - CompletableFuture socksF = new CompletableFuture<>(); - this.initOrReuseConn(socksF, keyString, finalLocal, remote); - - - return socksF.thenComposeAsync(v -> { - CompletableFuture dataF = new CompletableFuture<>(); - try { - udpSelectionKey = this.poolConn.getSocks5Conn().getUdpChannel().register(factory.getSelector(), SelectionKey.OP_READ); - udpSelectionKey.selector().wakeup(); - - udpSelectionKey.attach(new SendHandler(dataF, data, udpSelectionKey)); - udpSelectionKey.interestOps(SelectionKey.OP_WRITE); - udpSelectionKey.selector().wakeup(); - } catch (Exception e) { - dataF.completeExceptionally(e); - } - return dataF; - }).whenComplete((result, ex) -> { - try { - factory.unregisterFromSelector(poolConn, ex); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - private class SendHandler implements Runnable { - private final CompletableFuture future; - private final byte[] data; - private final SelectionKey selectionKey; - - public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { - this.future = future; - this.data = data; - this.selectionKey = selectionKey; - } - - @Override - public void run() { - try { - DatagramChannel channel = (DatagramChannel) selectionKey.channel(); - ByteBuffer buffer = poolConn.getSocks5Conn().addSocks5UdpAssociateHeader(data); - int headerLength = buffer.position()-data.length; - buffer.flip(); - while (buffer.hasRemaining()) { - channel.write(buffer); - } - selectionKey.attach(new ReceiveHandler(future, selectionKey, headerLength)); - selectionKey.interestOps(SelectionKey.OP_READ); - selectionKey.selector().wakeup(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } - - private class ReceiveHandler implements Runnable { - private final CompletableFuture future; - private final SelectionKey selectionKey; - private final int headerLength; - - public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey, int headerLength) { - this.future = future; - this.selectionKey = selectionKey; - this.headerLength = headerLength; - } - - @Override - public void run() { - try { - DatagramChannel channel = (DatagramChannel) selectionKey.channel(); - ByteBuffer responseData = ByteBuffer.allocate(max); - int read = channel.read(responseData); - if (read < 0) { - throw new EOFException(); - } - - int length = responseData.position() - headerLength; - byte[] data = new byte[length]; - responseData.position(headerLength); - responseData.get(data, 0, length); - future.complete(data); - - selectionKey.cancel(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } -} +//package org.xbill.DNS; +// +//import org.xbill.DNS.io.UdpIoClient; +// +//import java.io.EOFException; +//import java.io.IOException; +//import java.net.InetSocketAddress; +//import java.nio.ByteBuffer; +//import java.nio.channels.DatagramChannel; +//import java.nio.channels.SelectionKey; +//import java.nio.channels.SocketChannel; +//import java.time.Duration; +//import java.util.concurrent.CompletableFuture; +// +//public class Socks5ProxyUdpIoClient implements UdpIoClient { +// private final Socks5ProxyIoClientFactory factory; +// private Socks5ProxyIoClientFactory.PoolConn poolConn; +// private SelectionKey udpSelectionKey; +// private final Socks5ProxyConfig config; +// private int max; +// +// public Socks5ProxyUdpIoClient( +// Socks5ProxyIoClientFactory factory, +// Socks5ProxyConfig config) { +// this.factory = factory; +// this.config = config; +// } +// +// public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { +// Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); +// try { +// if (poolConn == null) { +// SocketChannel tcpConn = SocketChannel.open(); +// tcpConn.configureBlocking(false); +// SelectionKey selectionKey = factory.registerToSelector(tcpConn); +// Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.UDP_ASSOCIATE); +// tcpConn.connect(config.getProxyAddress()); +// socks5Conn.handleSOCKS5(f); +// this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); +// } else { +// SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); +// poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); +// this.poolConn = poolConn; +// f.complete(null); +// } +// } catch (IOException e) { +// f.completeExceptionally(e); +// } +// } +// +// +// @Override +// public CompletableFuture sendAndReceiveUdp( +// InetSocketAddress local, +// InetSocketAddress remote, +// Message query, +// byte[] data, +// int max, +// Duration timeout) { +// this.max = max; +// InetSocketAddress finalLocal; +// if (local == null) { +// finalLocal = new InetSocketAddress(0); +// } else { +// finalLocal = local; +// } +// // keyString is used to identify and reuse SOCKS5 connections +// String keyString = finalLocal.toString() + "-" + remote.toString() + "-UDP"; +// CompletableFuture socksF = new CompletableFuture<>(); +// this.initOrReuseConn(socksF, keyString, finalLocal, remote); +// +// +// return socksF.thenComposeAsync(v -> { +// CompletableFuture dataF = new CompletableFuture<>(); +// try { +// udpSelectionKey = this.poolConn.getSocks5Conn().getUdpChannel().register(factory.getSelector(), SelectionKey.OP_READ); +// udpSelectionKey.selector().wakeup(); +// +// udpSelectionKey.attach(new SendHandler(dataF, data, udpSelectionKey)); +// udpSelectionKey.interestOps(SelectionKey.OP_WRITE); +// udpSelectionKey.selector().wakeup(); +// } catch (Exception e) { +// dataF.completeExceptionally(e); +// } +// return dataF; +// }).whenComplete((result, ex) -> { +// try { +// factory.unregisterFromSelector(poolConn, ex); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } +// }); +// } +// +// private class SendHandler implements Runnable { +// private final CompletableFuture future; +// private final byte[] data; +// private final SelectionKey selectionKey; +// +// public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { +// this.future = future; +// this.data = data; +// this.selectionKey = selectionKey; +// } +// +// @Override +// public void run() { +// try { +// DatagramChannel channel = (DatagramChannel) selectionKey.channel(); +// ByteBuffer buffer = poolConn.getSocks5Conn().addSocks5UdpAssociateHeader(data); +// int headerLength = buffer.position()-data.length; +// buffer.flip(); +// while (buffer.hasRemaining()) { +// channel.write(buffer); +// } +// selectionKey.attach(new ReceiveHandler(future, selectionKey, headerLength)); +// selectionKey.interestOps(SelectionKey.OP_READ); +// selectionKey.selector().wakeup(); +// } catch (IOException e) { +// future.completeExceptionally(e); +// } +// } +// } +// +// private class ReceiveHandler implements Runnable { +// private final CompletableFuture future; +// private final SelectionKey selectionKey; +// private final int headerLength; +// +// public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey, int headerLength) { +// this.future = future; +// this.selectionKey = selectionKey; +// this.headerLength = headerLength; +// } +// +// @Override +// public void run() { +// try { +// DatagramChannel channel = (DatagramChannel) selectionKey.channel(); +// ByteBuffer responseData = ByteBuffer.allocate(max); +// int read = channel.read(responseData); +// if (read < 0) { +// throw new EOFException(); +// } +// +// int length = responseData.position() - headerLength; +// byte[] data = new byte[length]; +// responseData.position(headerLength); +// responseData.get(data, 0, length); +// future.complete(data); +// +// selectionKey.cancel(); +// } catch (IOException e) { +// future.completeExceptionally(e); +// } +// } +// } +//} From 8efe515d16d704fbc5352be02fd882c07d0e1442 Mon Sep 17 00:00:00 2001 From: thomasweiss Date: Sun, 12 Jan 2025 19:32:14 +0100 Subject: [PATCH 08/53] channel pool for UDP associate --- .../java/org/xbill/DNS/NioSocksUdpClient.java | 91 +++++++------ .../java/org/xbill/DNS/NioUdpHandler.java | 124 +++++++++++++----- 2 files changed, 138 insertions(+), 77 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index 11960159..a6ba33da 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -9,15 +9,13 @@ import java.nio.channels.DatagramChannel; import java.time.Duration; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @Slf4j final class NioSocksUdpClient extends NioClient implements UdpIoClient { - private static final NioTcpHandler tcpHandler = new NioTcpHandler(); - private static final NioUdpHandler udpHandler = new NioUdpHandler(); private final Socks5ProxyConfig socksConfig; - private static final Map channelMap = new ConcurrentHashMap<>(); NioSocksUdpClient(Socks5ProxyConfig config) { socksConfig = config; @@ -25,59 +23,58 @@ final class NioSocksUdpClient extends NioClient implements UdpIoClient { @Override public CompletableFuture sendAndReceiveUdp( - InetSocketAddress local, - InetSocketAddress remote, - Message query, - byte[] data, - int max, - Duration timeout) { - CompletableFuture f = new CompletableFuture<>(); + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + int max, + Duration timeout) { + CompletableFuture future = new CompletableFuture<>(); long endTime = System.nanoTime() + timeout.toNanos(); NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local); - NioTcpHandler.ChannelState tcpChannel = tcpHandler.createOrGetChannelState(local, remote, proxy, f); + NioTcpHandler.ChannelState tcpChannel = tcpHandler.createOrGetChannelState(local, remote, proxy, future); synchronized (tcpChannel) { if (tcpChannel.socks5HandshakeF == null) { tcpChannel.setSocks5(true); tcpChannel.socks5HandshakeF = proxy.doSocks5Handshake(tcpChannel, NioSocksHandler.SOCKS5_CMD_UDP_ASSOCIATE, query, endTime); } - tcpChannel.socks5HandshakeF.thenComposeAsync( - cmdBytes -> { - NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); - // newRemote is the UDP associate address - InetSocketAddress newRemote = new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); - byte[] wrappedData = proxy.addUdpHeader(data, newRemote); - DatagramChannel udpChannel = channelMap.computeIfAbsent(newRemote.toString(), k -> { - try { - return udpHandler.createChannel(local, newRemote, f); - } catch (Exception e) { - log.error("Failed to open UDP socket", e); - return null; - } - }); - udpHandler.sendAndReceiveUdp(local, newRemote, udpChannel, query, wrappedData, max, timeout).thenApplyAsync( - response -> { - if (response.length < 10) { - channelMap.remove(newRemote.toString()); - f.completeExceptionally(new IllegalStateException("SOCKS5 UDP response too short")); - } else { - // remove the SOCKS5 header from UDP response - f.complete(proxy.removeUdpHeader(response)); - } - return null; - } - ).exceptionally(ex -> { - channelMap.remove(newRemote.toString()); - f.completeExceptionally(ex); - return null; - }); - return CompletableFuture.completedFuture(null); + } + + tcpChannel.socks5HandshakeF.thenApplyAsync(cmdBytes -> { + NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); + InetSocketAddress newRemote = new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); + byte[] wrappedData = proxy.addUdpHeader(data, newRemote); + + DatagramChannel udpChannel; + udpChannel = channelMap.computeIfAbsent(newRemote.toString(), k -> { + try { + return udpHandler.createChannel(local, newRemote, future); + } catch (Exception e) { + future.completeExceptionally(e); + return null; } - ).exceptionally(ex -> { - f.completeExceptionally(ex); - return null; }); - return f; - } + + udpHandler.sendAndReceiveUdp(local, newRemote, udpChannel, query, wrappedData, max, timeout) + .thenApplyAsync(response -> { + if (response.length < 10) { + channelMap.remove(newRemote.toString()); + future.completeExceptionally(new IllegalStateException("SOCKS5 UDP response too short")); + } else { + future.complete(proxy.removeUdpHeader(response)); + } + return proxy.removeUdpHeader(response); + }).exceptionally(ex -> { + future.completeExceptionally(ex); + return null; + }); + return null; + }).exceptionally(ex -> { + future.completeExceptionally(ex); + return null; + }); + + return future; } } diff --git a/src/main/java/org/xbill/DNS/NioUdpHandler.java b/src/main/java/org/xbill/DNS/NioUdpHandler.java index 9461d19a..bc292b81 100644 --- a/src/main/java/org/xbill/DNS/NioUdpHandler.java +++ b/src/main/java/org/xbill/DNS/NioUdpHandler.java @@ -13,28 +13,26 @@ import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; import java.security.SecureRandom; import java.time.Duration; -import java.util.Iterator; -import java.util.Queue; +import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @Slf4j final class NioUdpHandler extends NioClient { private final int ephemeralStart; private final int ephemeralRange; - private final SecureRandom prng; - private final Queue registrationQueue = new ConcurrentLinkedQueue<>(); - private final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); + private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); + private static final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); NioUdpHandler() { - // https://datatracker.ietf.org/doc/html/rfc6335#section-6 int ephemeralStartDefault = 49152; int ephemeralEndDefault = 65535; - // Linux usually uses 32768-60999 if (System.getProperty("os.name").toLowerCase().contains("linux")) { ephemeralStartDefault = 32768; ephemeralEndDefault = 60999; @@ -81,6 +79,23 @@ private void checkTransactionTimeouts() { } } + private static void silentCloseChannel(DatagramChannel channel) { + if (channel != null) { + try { + channel.close(); + } catch (IOException ioe) { + // ignore + } + } + } + + private void closeUdp() { + registrationQueue.clear(); + EOFException closing = new EOFException("Client is closing"); + pendingTransactions.forEach(t -> t.completeExceptionally(closing)); + pendingTransactions.clear(); + } + @RequiredArgsConstructor private class Transaction implements KeyProcessor { private final int id; @@ -166,6 +181,77 @@ private void silentDisconnectAndCloseChannel() { } } + + public class SocksUdpAssociateChannelPool { + private final NioTcpHandler tcpHandler = new NioTcpHandler(); + private final NioUdpHandler udpHandler = new NioUdpHandler(); + private final Map channelMap = new ConcurrentHashMap<>(); + + public DatagramChannel createOrGetDatagramChannel( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture future) { + String key = local + " " + remote; + SocksUdpAssociateChannelGroup group = channelMap.computeIfAbsent(key, + k -> new SocksUdpAssociateChannelGroup(tcpHandler, udpHandler)); + return group.createOrGetDatagramChannel(local, remote, proxy, future); + } + } + + private class SocksUdpAssociateChannelGroup { + private final List channels; + private final NioTcpHandler tcpHandler; + private final NioUdpHandler udpHandler; + private final int defaultChannelIdleTimeout = 60000; + + public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { + channels = new ArrayList<>(); + this.tcpHandler = tcpHandler; + this.udpHandler = udpHandler; + } + + public synchronized DatagramChannel createOrGetDatagramChannel( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture future) { + SocksUdpAssociateChannelState channelState = channels.stream() + .filter(c -> !c.isOccupied) + .findFirst() + .orElseGet(() -> { + try { + SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); + newChannel.tcpChannel = tcpHandler.createOrGetChannelState(local, remote, proxy, future); + newChannel.udpChannel = udpHandler.createChannel(local, remote, future); + newChannel.poolChannelIdleTimeout = System.currentTimeMillis() + defaultChannelIdleTimeout; + channels.add(newChannel); + return newChannel; + } catch (IOException e) { + future.completeExceptionally(e); + return null; + } + }); + + if (channelState != null) { + channelState.isOccupied = true; + return channelState.udpChannel; + } else { + return null; + } + } + } + + @RequiredArgsConstructor + private class SocksUdpAssociateChannelState { + private NioTcpHandler.ChannelState tcpChannel; + private DatagramChannel udpChannel; + private boolean isOccupied = false; + private boolean isSocks5Initialized = false; + private long poolChannelIdleTimeout; + } + + public DatagramChannel createChannel(InetSocketAddress local, InetSocketAddress remote, CompletableFuture f) throws IOException { DatagramChannel channel = DatagramChannel.open(); channel.configureBlocking(false); @@ -216,13 +302,9 @@ public CompletableFuture sendAndReceiveUdp( CompletableFuture f = new CompletableFuture<>(); try { - boolean isProxyChannel = false; + boolean isProxyChannel = (channel != null); if (channel == null) { channel = createChannel(local, remote, f); - } else { - // Do not close the channel in case of SOCKS5 UDP associate. - // close it only on Exception and complete the future exceptionally. - isProxyChannel = true; } Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, channel, isProxyChannel, f); @@ -235,28 +317,10 @@ public CompletableFuture sendAndReceiveUdp( silentCloseChannel(channel); f.completeExceptionally(e); } catch (Throwable e) { - // Make sure to close the channel, no matter what, but only handle the declared IOException silentCloseChannel(channel); throw e; } return f; } - - private static void silentCloseChannel(DatagramChannel channel) { - if (channel != null) { - try { - channel.close(); - } catch (IOException ioe) { - // ignore - } - } - } - - private void closeUdp() { - registrationQueue.clear(); - EOFException closing = new EOFException("Client is closing"); - pendingTransactions.forEach(t -> t.completeExceptionally(closing)); - pendingTransactions.clear(); - } } From f9c1febc80d966a3e2cef049db774ce44c842563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 12 Jan 2025 20:43:02 +0100 Subject: [PATCH 09/53] integration the UDP channel pool --- .../java/org/xbill/DNS/NioSocksHandler.java | 21 ++++- .../DNS/NioSocksUdpAssociateChannelPool.java | 90 +++++++++++++++++++ .../java/org/xbill/DNS/NioSocksUdpClient.java | 35 ++++---- .../java/org/xbill/DNS/NioTcpHandler.java | 59 ++++++------ .../java/org/xbill/DNS/NioUdpHandler.java | 84 +++-------------- 5 files changed, 167 insertions(+), 122 deletions(-) create mode 100644 src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 900c6d67..95fee2d4 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -229,8 +229,25 @@ public byte[] addUdpHeader(byte[] data, InetSocketAddress to) { } public byte[] removeUdpHeader(byte[] in) { - byte[] out = new byte[in.length - 10]; - System.arraycopy(in, 10, out, 0, in.length - 10); + int addressType = in[3] & 0xFF; + int headerLength; + + switch (addressType) { + case SOCKS5_ATYP_IPV4: + headerLength = 10; + break; + case SOCKS5_ATYP_DOMAINNAME: + headerLength = 7 + (in[4] & 0xFF); + break; + case SOCKS5_ATYP_IPV6: + headerLength = 22; + break; + default: + throw new IllegalArgumentException("Unsupported address type: " + addressType); + } + + byte[] out = new byte[in.length - headerLength]; + System.arraycopy(in, headerLength, out, 0, in.length - headerLength); return out; } diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java new file mode 100644 index 00000000..a79ff7c1 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -0,0 +1,90 @@ +package org.xbill.DNS; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.DatagramChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +public class NioSocksUdpAssociateChannelPool { + private final NioTcpHandler tcpHandler; + private final NioUdpHandler udpHandler; + private final Map channelMap = new ConcurrentHashMap<>(); + + public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { + this.tcpHandler = tcpHandler; + this.udpHandler = udpHandler; + } + + public SocksUdpAssociateChannelState createOrGetChannelState( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture future) { + String key = local + " " + remote; + SocksUdpAssociateChannelGroup group = channelMap.computeIfAbsent(key, + k -> new SocksUdpAssociateChannelGroup(tcpHandler, udpHandler)); + return group.createOrGetDatagramChannel(local, remote, proxy, future); + } + + private static class SocksUdpAssociateChannelGroup { + private final List channels; + private final NioTcpHandler tcpHandler; + private final NioUdpHandler udpHandler; + private final int defaultChannelIdleTimeout = 60000; + + public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { + channels = new ArrayList<>(); + this.tcpHandler = tcpHandler; + this.udpHandler = udpHandler; + } + + public synchronized SocksUdpAssociateChannelState createOrGetDatagramChannel( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture future) { + SocksUdpAssociateChannelState channelState = channels.stream() + .filter(c -> !c.isOccupied) + .findFirst() + .orElseGet(() -> { + try { + SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); + newChannel.tcpChannel = tcpHandler.createChannelState(local, remote, proxy, future); + newChannel.udpChannel = udpHandler.createChannel(local, future); + newChannel.poolChannelIdleTimeout = System.currentTimeMillis() + defaultChannelIdleTimeout; + channels.add(newChannel); + return newChannel; + } catch (IOException e) { + future.completeExceptionally(e); + return null; + } + }); + + if (channelState != null) { + channelState.isOccupied = true; + return channelState; + } else { + return null; + } + } + } + + @RequiredArgsConstructor + @Getter + @Setter + public static class SocksUdpAssociateChannelState { + private NioTcpHandler.ChannelState tcpChannel; + private DatagramChannel udpChannel; + private boolean isOccupied = false; + private boolean isSocks5Initialized = false; + private long poolChannelIdleTimeout; + } +} diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index a6ba33da..1dab2fce 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -15,6 +15,9 @@ @Slf4j final class NioSocksUdpClient extends NioClient implements UdpIoClient { + private final NioTcpHandler tcpHandler = new NioTcpHandler(); + private final NioUdpHandler udpHandler = new NioUdpHandler(); + private final NioSocksUdpAssociateChannelPool udpPool = new NioSocksUdpAssociateChannelPool(tcpHandler, udpHandler); private final Socks5ProxyConfig socksConfig; NioSocksUdpClient(Socks5ProxyConfig config) { @@ -32,39 +35,33 @@ public CompletableFuture sendAndReceiveUdp( CompletableFuture future = new CompletableFuture<>(); long endTime = System.nanoTime() + timeout.toNanos(); NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local); - NioTcpHandler.ChannelState tcpChannel = tcpHandler.createOrGetChannelState(local, remote, proxy, future); + NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = udpPool.createOrGetChannelState(local, remote, proxy, future); - synchronized (tcpChannel) { - if (tcpChannel.socks5HandshakeF == null) { - tcpChannel.setSocks5(true); - tcpChannel.socks5HandshakeF = proxy.doSocks5Handshake(tcpChannel, NioSocksHandler.SOCKS5_CMD_UDP_ASSOCIATE, query, endTime); + synchronized (channel.getTcpChannel()) { + if (channel.getTcpChannel().socks5HandshakeF == null + || channel.getTcpChannel().socks5HandshakeF.isCompletedExceptionally() + || !channel.isSocks5Initialized()) { + channel.getTcpChannel().setSocks5(true); + channel.getTcpChannel().socks5HandshakeF = proxy.doSocks5Handshake( + channel.getTcpChannel(), NioSocksHandler.SOCKS5_CMD_UDP_ASSOCIATE, query, endTime); } } - tcpChannel.socks5HandshakeF.thenApplyAsync(cmdBytes -> { + channel.getTcpChannel().socks5HandshakeF.thenApplyAsync(cmdBytes -> { + channel.setSocks5Initialized(true); NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); InetSocketAddress newRemote = new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); byte[] wrappedData = proxy.addUdpHeader(data, newRemote); - DatagramChannel udpChannel; - udpChannel = channelMap.computeIfAbsent(newRemote.toString(), k -> { - try { - return udpHandler.createChannel(local, newRemote, future); - } catch (Exception e) { - future.completeExceptionally(e); - return null; - } - }); - - udpHandler.sendAndReceiveUdp(local, newRemote, udpChannel, query, wrappedData, max, timeout) + udpHandler.sendAndReceiveUdp(local, newRemote, channel.getUdpChannel(), query, wrappedData, max, timeout) .thenApplyAsync(response -> { + channel.setOccupied(false); if (response.length < 10) { - channelMap.remove(newRemote.toString()); future.completeExceptionally(new IllegalStateException("SOCKS5 UDP response too short")); } else { future.complete(proxy.removeUdpHeader(response)); } - return proxy.removeUdpHeader(response); + return null; }).exceptionally(ex -> { future.completeExceptionally(ex); return null; diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 6641fa6f..8d511f8b 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -312,37 +312,40 @@ public void dnsTransaction(ChannelState channel, Message query, byte[] data, lon channel.queueTransaction(t); } + public ChannelState createChannelState(InetSocketAddress local, InetSocketAddress remote, NioSocksHandler proxy, CompletableFuture f) { + log.debug("Opening async channel for l={}/r={}", local, remote); + SocketChannel c = null; + try { + c = SocketChannel.open(); + c.configureBlocking(false); + if (local != null) { + c.bind(local); + } + + if (proxy != null) { + c.connect(proxy.getProxyAddress()); + } else { + c.connect(remote); + } + return new ChannelState(c); + } catch (IOException e) { + if (c != null) { + try { + c.close(); + } catch (IOException ee) { + // ignore + } + } + f.completeExceptionally(e); + return null; + } + } + public ChannelState createOrGetChannelState(InetSocketAddress local, InetSocketAddress remote, NioSocksHandler proxy, CompletableFuture f) { return channelMap.computeIfAbsent( new ChannelKey(local, remote), - key -> { - log.debug("Opening async channel for l={}/r={}", local, remote); - SocketChannel c = null; - try { - c = SocketChannel.open(); - c.configureBlocking(false); - if (local != null) { - c.bind(local); - } - - if (proxy != null) { - c.connect(proxy.getProxyAddress()); - } else { - c.connect(remote); - } - return new ChannelState(c); - } catch (IOException e) { - if (c != null) { - try { - c.close(); - } catch (IOException ee) { - // ignore - } - } - f.completeExceptionally(e); - return null; - } - }); + key -> createChannelState(local, remote, proxy, f) + ); } public CompletableFuture sendAndReceiveTcp( diff --git a/src/main/java/org/xbill/DNS/NioUdpHandler.java b/src/main/java/org/xbill/DNS/NioUdpHandler.java index bc292b81..0ea8a9bc 100644 --- a/src/main/java/org/xbill/DNS/NioUdpHandler.java +++ b/src/main/java/org/xbill/DNS/NioUdpHandler.java @@ -182,77 +182,7 @@ private void silentDisconnectAndCloseChannel() { } - public class SocksUdpAssociateChannelPool { - private final NioTcpHandler tcpHandler = new NioTcpHandler(); - private final NioUdpHandler udpHandler = new NioUdpHandler(); - private final Map channelMap = new ConcurrentHashMap<>(); - - public DatagramChannel createOrGetDatagramChannel( - InetSocketAddress local, - InetSocketAddress remote, - NioSocksHandler proxy, - CompletableFuture future) { - String key = local + " " + remote; - SocksUdpAssociateChannelGroup group = channelMap.computeIfAbsent(key, - k -> new SocksUdpAssociateChannelGroup(tcpHandler, udpHandler)); - return group.createOrGetDatagramChannel(local, remote, proxy, future); - } - } - - private class SocksUdpAssociateChannelGroup { - private final List channels; - private final NioTcpHandler tcpHandler; - private final NioUdpHandler udpHandler; - private final int defaultChannelIdleTimeout = 60000; - - public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { - channels = new ArrayList<>(); - this.tcpHandler = tcpHandler; - this.udpHandler = udpHandler; - } - - public synchronized DatagramChannel createOrGetDatagramChannel( - InetSocketAddress local, - InetSocketAddress remote, - NioSocksHandler proxy, - CompletableFuture future) { - SocksUdpAssociateChannelState channelState = channels.stream() - .filter(c -> !c.isOccupied) - .findFirst() - .orElseGet(() -> { - try { - SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); - newChannel.tcpChannel = tcpHandler.createOrGetChannelState(local, remote, proxy, future); - newChannel.udpChannel = udpHandler.createChannel(local, remote, future); - newChannel.poolChannelIdleTimeout = System.currentTimeMillis() + defaultChannelIdleTimeout; - channels.add(newChannel); - return newChannel; - } catch (IOException e) { - future.completeExceptionally(e); - return null; - } - }); - - if (channelState != null) { - channelState.isOccupied = true; - return channelState.udpChannel; - } else { - return null; - } - } - } - - @RequiredArgsConstructor - private class SocksUdpAssociateChannelState { - private NioTcpHandler.ChannelState tcpChannel; - private DatagramChannel udpChannel; - private boolean isOccupied = false; - private boolean isSocks5Initialized = false; - private long poolChannelIdleTimeout; - } - - - public DatagramChannel createChannel(InetSocketAddress local, InetSocketAddress remote, CompletableFuture f) throws IOException { + public DatagramChannel createChannel(InetSocketAddress local, CompletableFuture f) throws IOException { DatagramChannel channel = DatagramChannel.open(); channel.configureBlocking(false); if (local == null || local.getPort() == 0) { @@ -287,7 +217,7 @@ public DatagramChannel createChannel(InetSocketAddress local, InetSocketAddress } else { channel.bind(local); } - return channel.connect(remote); + return channel; } public CompletableFuture sendAndReceiveUdp( @@ -304,7 +234,15 @@ public CompletableFuture sendAndReceiveUdp( try { boolean isProxyChannel = (channel != null); if (channel == null) { - channel = createChannel(local, remote, f); + channel = createChannel(local, f); + } + if (channel != null) { + if (!channel.isConnected()) { + channel.connect(remote); + } + } else { + f.completeExceptionally(new IOException("Could not create channel")); + return f; } Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, channel, isProxyChannel, f); From 9d0df8b55022b471c8eca910d65ed445eee17585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Fri, 17 Jan 2025 22:55:35 +0100 Subject: [PATCH 10/53] clean up of idle udp associate connections --- .../java/org/xbill/DNS/NioSocksHandler.java | 11 ++- .../DNS/NioSocksUdpAssociateChannelPool.java | 89 +++++++++++++------ .../java/org/xbill/DNS/NioSocksUdpClient.java | 25 ++---- .../java/org/xbill/DNS/NioTcpHandler.java | 12 ++- .../java/org/xbill/DNS/NioUdpHandler.java | 11 +++ 5 files changed, 100 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 95fee2d4..db3cf2ba 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -228,7 +228,11 @@ public byte[] addUdpHeader(byte[] data, InetSocketAddress to) { return buffer.array(); } - public byte[] removeUdpHeader(byte[] in) { + public byte[] removeUdpHeader(byte[] in) throws IllegalArgumentException { + if (in.length < 10) { + throw new IllegalArgumentException("SOCKS5 UDP response too short"); + } + int addressType = in[3] & 0xFF; int headerLength; @@ -306,7 +310,7 @@ public CmdRequest(byte command, InetSocketAddress address) { } else { addressType = SOCKS5_ATYP_DOMAINNAME; addressBytes = address.getHostName().getBytes(StandardCharsets.UTF_8); - bufferSize = 7 + addressBytes.length; + bufferSize = 6 + 1 + addressBytes.length; } port = (short) address.getPort(); } @@ -317,6 +321,9 @@ public byte[] toBytes() { buffer.put(this.command); buffer.put(this.reserved); buffer.put(this.addressType); + if (addressType == SOCKS5_ATYP_DOMAINNAME) { + buffer.put((byte) addressBytes.length); + } buffer.put(this.addressBytes); buffer.putShort(this.port); return buffer.array(); diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java index a79ff7c1..86e71048 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -3,16 +3,17 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.DatagramChannel; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +@Slf4j public class NioSocksUdpAssociateChannelPool { private final NioTcpHandler tcpHandler; private final NioUdpHandler udpHandler; @@ -23,7 +24,7 @@ public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler u this.udpHandler = udpHandler; } - public SocksUdpAssociateChannelState createOrGetChannelState( + public SocksUdpAssociateChannelState createOrGetSocketChannelState( InetSocketAddress local, InetSocketAddress remote, NioSocksHandler proxy, @@ -34,14 +35,29 @@ public SocksUdpAssociateChannelState createOrGetChannelState( return group.createOrGetDatagramChannel(local, remote, proxy, future); } + public void removeIdleChannels() { + long currentTime = System.currentTimeMillis(); + channelMap.values().forEach(group -> { + for (SocksUdpAssociateChannelState channel : group.channels) { + if (channel.poolChannelIdleTimeout < currentTime) { + try { + group.removeChannelState(channel); + } catch (IOException e) { + log.warn("Error closing idle channel", e); + } + } + } + }); + } + private static class SocksUdpAssociateChannelGroup { - private final List channels; + private final Queue channels; private final NioTcpHandler tcpHandler; private final NioUdpHandler udpHandler; private final int defaultChannelIdleTimeout = 60000; public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { - channels = new ArrayList<>(); + channels = new ConcurrentLinkedQueue<>(); this.tcpHandler = tcpHandler; this.udpHandler = udpHandler; } @@ -51,28 +67,40 @@ public synchronized SocksUdpAssociateChannelState createOrGetDatagramChannel( InetSocketAddress remote, NioSocksHandler proxy, CompletableFuture future) { - SocksUdpAssociateChannelState channelState = channels.stream() - .filter(c -> !c.isOccupied) - .findFirst() - .orElseGet(() -> { - try { - SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); - newChannel.tcpChannel = tcpHandler.createChannelState(local, remote, proxy, future); - newChannel.udpChannel = udpHandler.createChannel(local, future); - newChannel.poolChannelIdleTimeout = System.currentTimeMillis() + defaultChannelIdleTimeout; - channels.add(newChannel); - return newChannel; - } catch (IOException e) { - future.completeExceptionally(e); - return null; + SocksUdpAssociateChannelState channelState = null; + for (Iterator it = channels.iterator(); it.hasNext(); ) { + SocksUdpAssociateChannelState c = it.next(); + synchronized (c) { + if (!c.isOccupied) { + channelState = c; + c.occupy(); + break; } - }); + } + } - if (channelState != null) { - channelState.isOccupied = true; - return channelState; - } else { - return null; + if (channelState == null) { + try { + SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); + newChannel.tcpChannel = tcpHandler.createChannelState(local, remote, proxy, future); + newChannel.udpChannel = udpHandler.createChannel(local, future); + newChannel.poolChannelIdleTimeout = System.currentTimeMillis() + defaultChannelIdleTimeout; + newChannel.isOccupied = true; + channels.add(newChannel); + channelState = newChannel; + } catch (IOException e) { + future.completeExceptionally(e); + } + } + + return channelState; + } + + public void removeChannelState(SocksUdpAssociateChannelState channel) throws IOException { + if (channel.occupy()) { + channels.remove(channel); + channel.tcpChannel.close(); + channel.udpChannel.close(); } } } @@ -86,5 +114,14 @@ public static class SocksUdpAssociateChannelState { private boolean isOccupied = false; private boolean isSocks5Initialized = false; private long poolChannelIdleTimeout; + + public synchronized boolean occupy() { + if (!isOccupied) { + isOccupied = true; + return true; + } else { + return false; + } + } } } diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index 1dab2fce..f0f1b9ca 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -4,20 +4,13 @@ import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.io.UdpIoClient; -import java.net.DatagramSocket; import java.net.InetSocketAddress; -import java.nio.channels.DatagramChannel; import java.time.Duration; -import java.util.Map; -import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; @Slf4j final class NioSocksUdpClient extends NioClient implements UdpIoClient { - private final NioTcpHandler tcpHandler = new NioTcpHandler(); private final NioUdpHandler udpHandler = new NioUdpHandler(); - private final NioSocksUdpAssociateChannelPool udpPool = new NioSocksUdpAssociateChannelPool(tcpHandler, udpHandler); private final Socks5ProxyConfig socksConfig; NioSocksUdpClient(Socks5ProxyConfig config) { @@ -32,10 +25,10 @@ public CompletableFuture sendAndReceiveUdp( byte[] data, int max, Duration timeout) { - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture f = new CompletableFuture<>(); long endTime = System.nanoTime() + timeout.toNanos(); NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local); - NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = udpPool.createOrGetChannelState(local, remote, proxy, future); + NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = udpHandler.getUdpPool().createOrGetSocketChannelState(local, remote, proxy, f); synchronized (channel.getTcpChannel()) { if (channel.getTcpChannel().socks5HandshakeF == null @@ -56,22 +49,22 @@ public CompletableFuture sendAndReceiveUdp( udpHandler.sendAndReceiveUdp(local, newRemote, channel.getUdpChannel(), query, wrappedData, max, timeout) .thenApplyAsync(response -> { channel.setOccupied(false); - if (response.length < 10) { - future.completeExceptionally(new IllegalStateException("SOCKS5 UDP response too short")); - } else { - future.complete(proxy.removeUdpHeader(response)); + try { + f.complete(proxy.removeUdpHeader(response)); + } catch (IllegalArgumentException e) { + f.completeExceptionally(e); } return null; }).exceptionally(ex -> { - future.completeExceptionally(ex); + f.completeExceptionally(ex); return null; }); return null; }).exceptionally(ex -> { - future.completeExceptionally(ex); + f.completeExceptionally(ex); return null; }); - return future; + return f; } } diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 8d511f8b..0a3922c5 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -153,6 +153,10 @@ void handleTransactionException(IOException e) { private void handleChannelException(IOException e) { handleTransactionException(e); + close(); + } + + public void close() { for (Map.Entry entry : channelMap.entrySet()) { if (entry.getValue() == this) { channelMap.remove(entry.getKey()); @@ -160,10 +164,10 @@ private void handleChannelException(IOException e) { channel.close(); } catch (IOException ex) { log.warn( - "Failed to close channel l={}/r={}", - entry.getKey().local, - entry.getKey().remote, - ex); + "Failed to close channel l={}/r={}", + entry.getKey().local, + entry.getKey().remote, + ex); } return; } diff --git a/src/main/java/org/xbill/DNS/NioUdpHandler.java b/src/main/java/org/xbill/DNS/NioUdpHandler.java index 0ea8a9bc..5ffe2234 100644 --- a/src/main/java/org/xbill/DNS/NioUdpHandler.java +++ b/src/main/java/org/xbill/DNS/NioUdpHandler.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,6 +23,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; @Slf4j +@Getter final class NioUdpHandler extends NioClient { private final int ephemeralStart; private final int ephemeralRange; @@ -29,6 +31,8 @@ final class NioUdpHandler extends NioClient { private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); private static final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); + private final NioSocksUdpAssociateChannelPool udpPool; + NioUdpHandler() { int ephemeralStartDefault = 49152; int ephemeralEndDefault = 65535; @@ -47,6 +51,10 @@ final class NioUdpHandler extends NioClient { } else { prng = new SecureRandom(); } + + // NioTcpHandler for SOCKS5 UDP associate + udpPool = new NioSocksUdpAssociateChannelPool(new NioTcpHandler(), this); + setRegistrationsTask(this::processPendingRegistrations, false); setTimeoutTask(this::checkTransactionTimeouts, false); setCloseTask(this::closeUdp, false); @@ -77,6 +85,9 @@ private void checkTransactionTimeouts() { it.remove(); } } + + // check for idle channels and remove them + udpPool.removeIdleChannels(); } private static void silentCloseChannel(DatagramChannel channel) { From 44f441561817043432ae850cefa1aaa0d5ab2e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 18 Jan 2025 20:26:23 +0100 Subject: [PATCH 11/53] further cleanup of the NioSocksHandler --- .../java/org/xbill/DNS/NioSocksHandler.java | 271 +++++++++--------- .../java/org/xbill/DNS/NioSocksTcpClient.java | 2 +- .../java/org/xbill/DNS/NioSocksUdpClient.java | 2 +- 3 files changed, 145 insertions(+), 130 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index db3cf2ba..3663ffe9 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -1,6 +1,7 @@ package org.xbill.DNS; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import java.net.Inet4Address; import java.net.Inet6Address; @@ -11,6 +12,7 @@ import java.util.concurrent.CompletableFuture; @Getter +@Slf4j public class NioSocksHandler { private static final byte SOCKS5_VERSION = 0x05; private static final byte SOCKS5_USER_PWD_AUTH_VERSION = 0x01; @@ -47,7 +49,7 @@ public class NioSocksHandler { public NioSocksHandler(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress, String socks5User, String socks5Password) { this.remoteAddress = Objects.requireNonNull(remoteAddress, "remoteAddress must not be null"); - this.localAddress = localAddress; //Objects.requireNonNull(localAddress, "localAddress must not be null"); + this.localAddress = localAddress; this.proxyAddress = Objects.requireNonNull(proxyAddress, "proxyAddress must not be null"); this.socks5User = socks5User; this.socks5Password = socks5Password; @@ -63,98 +65,75 @@ private MethodSelectionRequest getMethodSelectionRequest() { public CompletableFuture doAuthHandshake(NioTcpHandler.ChannelState channel, Message query, long endTime) { CompletableFuture authHandshakeF = new CompletableFuture<>(); - - // SOCKS5 method selection transaction CompletableFuture methodSelectionF = new CompletableFuture<>(); - NioSocksHandler.MethodSelectionRequest methodSelectionRequest = getMethodSelectionRequest(); - NioTcpHandler.Transaction methodSelectionTransaction = new NioTcpHandler.Transaction( - query, methodSelectionRequest.toBytes(), endTime, channel.getChannel(), methodSelectionF); + MethodSelectionRequest methodSelectionRequest = getMethodSelectionRequest(); + NioTcpHandler.Transaction methodSelectionTransaction = new NioTcpHandler.Transaction(query, methodSelectionRequest.toBytes(), endTime, channel.getChannel(), methodSelectionF); channel.queueTransaction(methodSelectionTransaction); - methodSelectionF.thenComposeAsync( - methodSelectionBytes -> { - if (methodSelectionBytes.length != 2) { - authHandshakeF.completeExceptionally(new UnsupportedOperationException("Invalid SOCKS5 method selection response")); - } - NioSocksHandler.MethodSelectionResponse methodSelectionResponse = new NioSocksHandler.MethodSelectionResponse(methodSelectionBytes); - if (methodSelectionResponse.getMethod() == NioSocksHandler.SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { - authHandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); - } else { - if (methodSelectionResponse.getMethod() == NioSocksHandler.SOCKS5_AUTH_NONE) { - authHandshakeF.complete(null); - } -// else if (methodSelectionResponse.getMethod() == NioSocksHandler.SOCKS5_AUTH_USER_PASS) { -// // SOCKS5 authentication transaction (if required) -// CompletableFuture userPassAuthF = new CompletableFuture<>(); -// UserPassAuthRequest userPassAuthRequest = getUserPassAuthRequest(); -// NioTcpHandler.Transaction userPwdAuthTransaction = NioTcpHandler.Transaction(query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassF); -// channel.queueTransaction(userPwdAuthTransaction); -// userPassAuthF.thenComposeAsync( -// authIn -> { -// CompletableFuture authF = new CompletableFuture<>(); -// UserPwdAuthResponse userPwdAuthResponse = UserPwdAuthResponse.fromBytes(authIn); -// if (userPwdAuthResponse.getStatus() != NioSocksHandler.SOCKS5_REP_SUCCEEDED) { -// authHandshakeF.completeExceptionally( -// new UnsupportedOperationException("SOCKS5 user/pwd authentication failed with status: " + userPwdAuthResponse.getStatus())); -// } else { -// authF.complete(authIn); -// } -// return authF; -// } -// ); -// } - } + + methodSelectionF.thenComposeAsync(methodSelectionBytes -> { + if (methodSelectionBytes.length != 2) { + authHandshakeF.completeExceptionally(new UnsupportedOperationException("Invalid SOCKS5 method selection response")); + return null; + } + MethodSelectionResponse methodSelectionResponse = new MethodSelectionResponse(methodSelectionBytes); + if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { + authHandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); return null; } - ); + if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_NONE) { + authHandshakeF.complete(null); + } else if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_USER_PASS) { + return handleUserPassAuth(channel, query, endTime, authHandshakeF); + } else if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_GSSAPI) { + // TODO: Implement GSSAPI + authHandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); + } + return null; + }); return authHandshakeF; } - public CompletableFuture doConnectHandshake(NioTcpHandler.ChannelState channel, Message query, long endTime) { - CompletableFuture cmdHandshakeF = new CompletableFuture<>(); + private CompletableFuture handleUserPassAuth(NioTcpHandler.ChannelState channel, Message query, long endTime, CompletableFuture authHandshakeF) { + CompletableFuture userPassAuthF = new CompletableFuture<>(); + UserPassAuthRequest userPassAuthRequest = new UserPassAuthRequest(socks5User, socks5Password); + NioTcpHandler.Transaction userPwdAuthTransaction = new NioTcpHandler.Transaction(query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassAuthF); + channel.queueTransaction(userPwdAuthTransaction); - // SOCKS5 cmd transaction - CompletableFuture commandF = new CompletableFuture<>(); - CmdRequest cmdRequest = new CmdRequest(SOCKS5_CMD_CONNECT, remoteAddress); - NioTcpHandler.Transaction commandTransaction = new NioTcpHandler.Transaction( - query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); - channel.queueTransaction(commandTransaction); - commandF.thenComposeAsync( - in -> { - CmdResponse cmdResponse = new CmdResponse(in); - if (cmdResponse.getReply() != NioSocksHandler.SOCKS5_REP_SUCCEEDED) { - cmdHandshakeF.completeExceptionally( - new UnsupportedOperationException("SOCKS5 command failed with status: " + cmdResponse.getReply())); - } else { - cmdHandshakeF.complete(in); - } - return null; + userPassAuthF.thenComposeAsync(authIn -> { + UserPwdAuthResponse userPwdAuthResponse = new UserPwdAuthResponse(authIn); + if (userPwdAuthResponse.getStatus() != SOCKS5_REP_SUCCEEDED) { + authHandshakeF.completeExceptionally(new UnsupportedOperationException("SOCKS5 user/pwd authentication failed with status: " + userPwdAuthResponse.getStatus())); + } else { + authHandshakeF.complete(null); } - ); + return null; + }); - return cmdHandshakeF; + return authHandshakeF; } - public CompletableFuture doUdpAssociateHandshake(NioTcpHandler.ChannelState channel, Message query, long endTime) { - CompletableFuture cmdHandshakeF = new CompletableFuture<>(); - // SOCKS5 cmd transaction + public CompletableFuture doSocks5Request(NioTcpHandler.ChannelState channel, byte socks5Cmd, Message query, long endTime) { + CompletableFuture cmdHandshakeF = new CompletableFuture<>(); CompletableFuture commandF = new CompletableFuture<>(); - CmdRequest cmdRequest = new CmdRequest(SOCKS5_CMD_UDP_ASSOCIATE, new InetSocketAddress("0.0.0.0", 0)); - NioTcpHandler.Transaction commandTransaction = new NioTcpHandler.Transaction( - query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); + // For CONNECT, DST.ADDR and DST.PORT are the address and port of the destination server. + // For UDP ASSOCIATE, DST.ADDR and DST.PORT are the address and port of the UDP client. + // If DST.ADDR and DST.PORT are set to 0.0.0.0:0, the proxy will accept UDP connections from any source address and port. + // After the first packet, the source address and port must not change. If they change, the proxy drops the connection and the UDP association. + InetSocketAddress address = (socks5Cmd == SOCKS5_CMD_CONNECT) ? remoteAddress : new InetSocketAddress("0.0.0.0", 0); + CmdRequest cmdRequest = new CmdRequest(socks5Cmd, address); + NioTcpHandler.Transaction commandTransaction = new NioTcpHandler.Transaction(query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); channel.queueTransaction(commandTransaction); - commandF.thenComposeAsync( - in -> { - CmdResponse cmdResponse = new CmdResponse(in); - if (cmdResponse.getReply() != NioSocksHandler.SOCKS5_REP_SUCCEEDED) { - cmdHandshakeF.completeExceptionally( - new UnsupportedOperationException("SOCKS5 command failed with status: " + cmdResponse.getReply())); - } else { - cmdHandshakeF.complete(in); - } - return null; + + commandF.thenComposeAsync(in -> { + CmdResponse cmdResponse = new CmdResponse(in); + if (cmdResponse.getReply() != SOCKS5_REP_SUCCEEDED) { + cmdHandshakeF.completeExceptionally(new UnsupportedOperationException("SOCKS5 command failed with status: " + cmdResponse.getReply())); + } else { + cmdHandshakeF.complete(in); } - ); + return null; + }); return cmdHandshakeF; } @@ -164,34 +143,24 @@ public synchronized CompletableFuture doSocks5Handshake(NioTcpHandler.Ch channel.setSocks5(true); CompletableFuture authHandshakeF = doAuthHandshake(channel, query, endTime); - authHandshakeF.thenRunAsync( - () -> { - CompletableFuture cmdHandshakeF; - if (socks5Cmd == SOCKS5_CMD_CONNECT) { - cmdHandshakeF = doConnectHandshake(channel, query, endTime); - } else if (socks5Cmd == SOCKS5_CMD_UDP_ASSOCIATE) { - cmdHandshakeF = doUdpAssociateHandshake(channel, query, endTime); - } else { - cmdHandshakeF = CompletableFuture.failedFuture(new UnsupportedOperationException("Unsupported SOCKS5 command: " + socks5Cmd)); - } - cmdHandshakeF.thenComposeAsync( - in -> { - socks5HandshakeF.complete(in); - return null; - } - ).exceptionally( - e -> { - socks5HandshakeF.completeExceptionally(e); - return null; - } - ); + authHandshakeF.thenRunAsync(() -> { + CompletableFuture cmdHandshakeF; + if (socks5Cmd == SOCKS5_CMD_CONNECT || socks5Cmd == SOCKS5_CMD_UDP_ASSOCIATE) { + cmdHandshakeF = doSocks5Request(channel, socks5Cmd, query, endTime); + } else { + cmdHandshakeF = CompletableFuture.failedFuture(new UnsupportedOperationException("Unsupported SOCKS5 command: " + socks5Cmd)); } - ).exceptionally( - e -> { + cmdHandshakeF.thenComposeAsync(in -> { + socks5HandshakeF.complete(in); + return null; + }).exceptionally(e -> { socks5HandshakeF.completeExceptionally(e); return null; - } - ); + }); + }).exceptionally(e -> { + socks5HandshakeF.completeExceptionally(e); + return null; + }); return socks5HandshakeF; } @@ -217,7 +186,7 @@ public byte[] addUdpHeader(byte[] data, InetSocketAddress to) { buffer.put((byte) 0x00); // RSV buffer.put((byte) 0x00); // RSV buffer.put((byte) 0x00); // FRAG - buffer.put(addressType); // ATYP (IPv4) + buffer.put(addressType); // ATYP if (addressType == SOCKS5_ATYP_DOMAINNAME) { buffer.put((byte) addressBytes.length); } @@ -235,7 +204,6 @@ public byte[] removeUdpHeader(byte[] in) throws IllegalArgumentException { int addressType = in[3] & 0xFF; int headerLength; - switch (addressType) { case SOCKS5_ATYP_IPV4: headerLength = 10; @@ -255,7 +223,6 @@ public byte[] removeUdpHeader(byte[] in) throws IllegalArgumentException { return out; } - static class MethodSelectionRequest { private final byte version; private final byte method; @@ -264,6 +231,7 @@ public MethodSelectionRequest(byte method) { this.version = SOCKS5_VERSION; this.method = method; } + public byte[] toBytes() { ByteBuffer buffer = ByteBuffer.allocate(3); buffer.put(this.version); @@ -280,8 +248,46 @@ static class MethodSelectionResponse { public MethodSelectionResponse(byte[] methodSelectionBytes) { ByteBuffer buffer = ByteBuffer.wrap(methodSelectionBytes); - version = buffer.get();; - method = buffer.get();; + version = buffer.get(); + method = buffer.get(); + } + } + + static class UserPassAuthRequest { + private final byte version; + private final byte usernameLength; + private final byte[] username; + private final byte passwordLength; + private final byte[] password; + + public UserPassAuthRequest(String username, String password) { + this.version = SOCKS5_USER_PWD_AUTH_VERSION; + this.username = username.getBytes(StandardCharsets.UTF_8); + this.usernameLength = (byte) this.username.length; + this.password = password.getBytes(StandardCharsets.UTF_8); + this.passwordLength = (byte) this.password.length; + } + + public byte[] toBytes() { + ByteBuffer buffer = ByteBuffer.allocate(3 + username.length + password.length); + buffer.put(this.version); + buffer.put(this.usernameLength); + buffer.put(this.username); + buffer.put(this.passwordLength); + buffer.put(this.password); + return buffer.array(); + } + } + + @Getter + static class UserPwdAuthResponse { + private final byte version; + private final byte status; + + public UserPwdAuthResponse(byte[] userPwdAuthResponseBytes) { + ByteBuffer buffer = ByteBuffer.wrap(userPwdAuthResponseBytes); + version = buffer.get(); + status = buffer.get(); } } @@ -292,27 +298,26 @@ static class CmdRequest { private final byte addressType; private final byte[] addressBytes; private final short port; - private final int bufferSize; public CmdRequest(byte command, InetSocketAddress address) { - version = SOCKS5_VERSION; + this.version = SOCKS5_VERSION; this.command = command; - reserved = SOCKS5_RESERVED; + this.reserved = SOCKS5_RESERVED; if (address.getAddress() instanceof Inet4Address) { - addressType = SOCKS5_ATYP_IPV4; - addressBytes = address.getAddress().getAddress(); - bufferSize = 10; + this.addressType = SOCKS5_ATYP_IPV4; + this.addressBytes = address.getAddress().getAddress(); + this.bufferSize = 10; } else if (address.getAddress() instanceof Inet6Address) { - addressType = SOCKS5_ATYP_IPV6; - addressBytes = address.getAddress().getAddress(); - bufferSize = 22; + this.addressType = SOCKS5_ATYP_IPV6; + this.addressBytes = address.getAddress().getAddress(); + this.bufferSize = 22; } else { - addressType = SOCKS5_ATYP_DOMAINNAME; - addressBytes = address.getHostName().getBytes(StandardCharsets.UTF_8); - bufferSize = 6 + 1 + addressBytes.length; + this.addressType = SOCKS5_ATYP_DOMAINNAME; + this.addressBytes = address.getHostName().getBytes(StandardCharsets.UTF_8); + this.bufferSize = 6 + 1 + addressBytes.length; } - port = (short) address.getPort(); + this.port = (short) address.getPort(); } public byte[] toBytes() { @@ -341,14 +346,24 @@ static class CmdResponse { public CmdResponse(byte[] commandResponseBytes) { ByteBuffer buffer = ByteBuffer.wrap(commandResponseBytes); - version = buffer.get(); - reply = buffer.get(); - reserved = buffer.get(); - addressType = buffer.get(); - address = new byte[addressType == SOCKS5_ATYP_IPV4 ? 4 : 16]; - buffer.get(address); - // Short.toUnsignedInt makes a difference for port numbers higher than 32767 - port = Short.toUnsignedInt(buffer.getShort()); + this.version = buffer.get(); + this.reply = buffer.get(); + this.reserved = buffer.get(); + this.addressType = buffer.get(); + + if (addressType == SOCKS5_ATYP_IPV4) { + this.address = new byte[4]; + } else if (addressType == SOCKS5_ATYP_IPV6) { + this.address = new byte[16]; + } else if (addressType == SOCKS5_ATYP_DOMAINNAME) { + int domainLength = buffer.get() & 0xFF; + this.address = new byte[domainLength]; + } else { + throw new IllegalArgumentException("Unsupported address type: " + addressType); + } + + buffer.get(this.address); + this.port = Short.toUnsignedInt(buffer.getShort()); } } } diff --git a/src/main/java/org/xbill/DNS/NioSocksTcpClient.java b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java index eedf9365..7fea3a29 100644 --- a/src/main/java/org/xbill/DNS/NioSocksTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java @@ -31,7 +31,7 @@ public CompletableFuture sendAndReceiveTcp( Message query, byte[] data, Duration timeout) { - NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local); + NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local, socksConfig.getSocks5User(), socksConfig.getSocks5Password()); return tcpHandler.sendAndReceiveTcp(local, remote, proxy, query, data, timeout); } } diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index f0f1b9ca..e56a5c61 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -27,7 +27,7 @@ public CompletableFuture sendAndReceiveUdp( Duration timeout) { CompletableFuture f = new CompletableFuture<>(); long endTime = System.nanoTime() + timeout.toNanos(); - NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local); + NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local, socksConfig.getSocks5User(), socksConfig.getSocks5Password()); NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = udpHandler.getUdpPool().createOrGetSocketChannelState(local, remote, proxy, f); synchronized (channel.getTcpChannel()) { From 4cfaf31a2631bf4ebfcb7abd2b1e96e22be783b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 18 Jan 2025 20:30:17 +0100 Subject: [PATCH 12/53] better error handling --- .../java/org/xbill/DNS/NioSocksHandler.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 3663ffe9..3439fb62 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -127,8 +127,37 @@ public CompletableFuture doSocks5Request(NioTcpHandler.ChannelState chan commandF.thenComposeAsync(in -> { CmdResponse cmdResponse = new CmdResponse(in); - if (cmdResponse.getReply() != SOCKS5_REP_SUCCEEDED) { - cmdHandshakeF.completeExceptionally(new UnsupportedOperationException("SOCKS5 command failed with status: " + cmdResponse.getReply())); + if (cmdResponse.getReply() != SOCKS5_REP_SUCCEEDED) { + String errorMessage; + switch (cmdResponse.getReply()) { + case SOCKS5_REP_GENERAL_FAILURE: + errorMessage = "General SOCKS server failure"; + break; + case SOCKS5_REP_CONNECTION_NOT_ALLOWED: + errorMessage = "Connection not allowed by ruleset"; + break; + case SOCKS5_REP_NETWORK_UNREACHABLE: + errorMessage = "Network unreachable"; + break; + case SOCKS5_REP_HOST_UNREACHABLE: + errorMessage = "Host unreachable"; + break; + case SOCKS5_REP_CONNECTION_REFUSED: + errorMessage = "Connection refused by destination host"; + break; + case SOCKS5_REP_TTL_EXPIRED: + errorMessage = "TTL expired"; + break; + case SOCKS5_REP_COMMAND_NOT_SUPPORTED: + errorMessage = "Command not supported"; + break; + case SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED: + errorMessage = "Address type not supported"; + break; + default: + errorMessage = "Unknown SOCKS5 error with status: " + cmdResponse.getReply(); + } + cmdHandshakeF.completeExceptionally(new UnsupportedOperationException("SOCKS5 command failed: " + errorMessage)); } else { cmdHandshakeF.complete(in); } From 07a6977da69815da46998382f15a59763a8a8dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 18 Jan 2025 20:48:25 +0100 Subject: [PATCH 13/53] cleanup --- .../java/org/xbill/DNS/NioTcpHandler.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 0a3922c5..47aa3f36 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -376,19 +376,18 @@ public CompletableFuture sendAndReceiveTcp( channel.setSocks5(true); channel.socks5HandshakeF = proxy.doSocks5Handshake(channel, NioSocksHandler.SOCKS5_CMD_CONNECT, query, endTime); } - // Chain the SOCKS5 transactions with the main data transaction - channel.socks5HandshakeF.thenRunAsync( - () -> { - dnsTransaction(channel, query, data, endTime, f); - } - ).exceptionally(ex -> { - channel.socks5HandshakeF = null; - f.completeExceptionally(ex); - return null; - }); } + // Chain the SOCKS5 transactions with the TCP data transaction + channel.socks5HandshakeF.thenRunAsync( + () -> { + dnsTransaction(channel, query, data, endTime, f); + } + ).exceptionally(ex -> { + f.completeExceptionally(ex); + return null; + }); } else { - // main DNS data transaction + // transaction for DNS data dnsTransaction(channel, query, data, endTime, f); } } From c365e36711edc8da9e4ff1bf1898099bf9b948d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 18 Jan 2025 21:03:52 +0100 Subject: [PATCH 14/53] adjustments for the socks5 handshake future --- .../java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java | 1 - src/main/java/org/xbill/DNS/NioSocksUdpClient.java | 4 +--- src/main/java/org/xbill/DNS/NioTcpHandler.java | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java index 86e71048..060bdb7e 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -112,7 +112,6 @@ public static class SocksUdpAssociateChannelState { private NioTcpHandler.ChannelState tcpChannel; private DatagramChannel udpChannel; private boolean isOccupied = false; - private boolean isSocks5Initialized = false; private long poolChannelIdleTimeout; public synchronized boolean occupy() { diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index e56a5c61..80c81773 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -32,8 +32,7 @@ public CompletableFuture sendAndReceiveUdp( synchronized (channel.getTcpChannel()) { if (channel.getTcpChannel().socks5HandshakeF == null - || channel.getTcpChannel().socks5HandshakeF.isCompletedExceptionally() - || !channel.isSocks5Initialized()) { + || channel.getTcpChannel().socks5HandshakeF.isCompletedExceptionally()) { channel.getTcpChannel().setSocks5(true); channel.getTcpChannel().socks5HandshakeF = proxy.doSocks5Handshake( channel.getTcpChannel(), NioSocksHandler.SOCKS5_CMD_UDP_ASSOCIATE, query, endTime); @@ -41,7 +40,6 @@ public CompletableFuture sendAndReceiveUdp( } channel.getTcpChannel().socks5HandshakeF.thenApplyAsync(cmdBytes -> { - channel.setSocks5Initialized(true); NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); InetSocketAddress newRemote = new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); byte[] wrappedData = proxy.addUdpHeader(data, newRemote); diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 47aa3f36..cab2a1d4 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -372,7 +372,7 @@ public CompletableFuture sendAndReceiveTcp( long endTime = System.nanoTime() + timeout.toNanos(); if (proxy != null) { synchronized (channel) { - if (channel.socks5HandshakeF == null) { + if (channel.socks5HandshakeF == null || channel.socks5HandshakeF.isCompletedExceptionally()) { channel.setSocks5(true); channel.socks5HandshakeF = proxy.doSocks5Handshake(channel, NioSocksHandler.SOCKS5_CMD_CONNECT, query, endTime); } From 260c4d5d38c14396a310186ae4aca4ef0e5b258c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 18 Jan 2025 21:10:40 +0100 Subject: [PATCH 15/53] Make compatible with Java 8 --- src/main/java/org/xbill/DNS/NioSocksHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 3439fb62..92782a22 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -177,7 +177,8 @@ public synchronized CompletableFuture doSocks5Handshake(NioTcpHandler.Ch if (socks5Cmd == SOCKS5_CMD_CONNECT || socks5Cmd == SOCKS5_CMD_UDP_ASSOCIATE) { cmdHandshakeF = doSocks5Request(channel, socks5Cmd, query, endTime); } else { - cmdHandshakeF = CompletableFuture.failedFuture(new UnsupportedOperationException("Unsupported SOCKS5 command: " + socks5Cmd)); + socks5HandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 command: " + socks5Cmd)); + return; } cmdHandshakeF.thenComposeAsync(in -> { socks5HandshakeF.complete(in); From 02c31688a349f9f2524cf6024543a6ab45a36d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 22:09:54 +0100 Subject: [PATCH 16/53] adjusting changes to upstream --- .../java/org/xbill/DNS/NioTcpHandler.java | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index cab2a1d4..362caf3e 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -83,35 +83,51 @@ public static class Transaction { private final long endTime; private final SocketChannel channel; private final CompletableFuture f; - private boolean sendDone; + private ByteBuffer queryDataBuffer; + long bytesWrittenTotal = 0; - void send() throws IOException { - if (sendDone) { - return; + boolean send() throws IOException { + // send can be invoked multiple times if the entire buffer couldn't be written at once + if (bytesWrittenTotal == queryData.length + 2) { + return true; + } + if (queryDataBuffer == null) { + // combine length+message to avoid multiple TCP packets + // https://datatracker.ietf.org/doc/html/rfc7766#section-8 + queryDataBuffer = ByteBuffer.allocate(queryData.length + 2); + queryDataBuffer.put((byte) (queryData.length >>> 8)); + queryDataBuffer.put((byte) (queryData.length & 0xFF)); + queryDataBuffer.put(queryData); + queryDataBuffer.flip(); } verboseLog( "TCP write: transaction id=" + query.getHeader().getID(), channel.socket().getLocalSocketAddress(), channel.socket().getRemoteSocketAddress(), - queryData); - - ByteBuffer buffer = ByteBuffer.allocate(queryData.length); - buffer.put(queryData); - buffer.flip(); - while (buffer.hasRemaining()) { - long n = channel.write(buffer); - if (n == 0) { - throw new EOFException( - "Insufficient room for the data in the underlying output buffer for transaction " - + query.getHeader().getID()); - } else if (n < queryData.length) { - throw new EOFException( - "Could not write all data for transaction " + query.getHeader().getID()); + queryDataBuffer); + while (queryDataBuffer.hasRemaining()) { + long bytesWritten = channel.write(queryDataBuffer); + bytesWrittenTotal += bytesWritten; + if (bytesWritten == 0) { + log.debug( + "Insufficient room for the data in the underlying output buffer for transaction {}, retrying", + query.getHeader().getID()); + return false; + } else if (bytesWrittenTotal < queryData.length) { + log.debug( + "Wrote {} of {} bytes data for transaction {}", + bytesWrittenTotal, + queryData.length, + query.getHeader().getID()); } } - sendDone = true; + log.debug( + "Send for transaction {} is complete, wrote {} bytes", + query.getHeader().getID(), + bytesWrittenTotal); + return true; } } @@ -274,7 +290,12 @@ private void processWrite(SelectionKey key) { for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { Transaction t = it.next(); try { - t.send(); + if (!t.send()) { + // Write was incomplete because the output buffer was full. Wait until the selector + // tells us that we can write again + key.interestOps(SelectionKey.OP_WRITE); + return; + } } catch (IOException e) { t.f.completeExceptionally(e); it.remove(); From 588df3d362f9c52dbc77c1efea41829f9c455212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 13:34:40 +0100 Subject: [PATCH 17/53] clean up --- ...yConfig.java => NioSocks5ProxyConfig.java} | 17 +- .../org/xbill/DNS/NioSocks5ProxyFactory.java | 19 +- .../java/org/xbill/DNS/NioSocksHandler.java | 257 ++++++----- .../java/org/xbill/DNS/NioSocksTcpClient.java | 20 +- .../DNS/NioSocksUdpAssociateChannelPool.java | 67 +-- .../java/org/xbill/DNS/NioSocksUdpClient.java | 95 ++-- src/main/java/org/xbill/DNS/NioTcpClient.java | 1 - .../java/org/xbill/DNS/NioTcpHandler.java | 91 ++-- src/main/java/org/xbill/DNS/NioUdpClient.java | 14 - .../java/org/xbill/DNS/NioUdpHandler.java | 64 +-- src/main/java/org/xbill/DNS/Socks5Proxy.java | 432 ------------------ .../xbill/DNS/Socks5ProxyIoClientFactory.java | 236 ---------- .../org/xbill/DNS/Socks5ProxyTcpIoClient.java | 156 ------- .../org/xbill/DNS/Socks5ProxyUdpIoClient.java | 158 ------- 14 files changed, 358 insertions(+), 1269 deletions(-) rename src/main/java/org/xbill/DNS/{Socks5ProxyConfig.java => NioSocks5ProxyConfig.java} (57%) delete mode 100644 src/main/java/org/xbill/DNS/Socks5Proxy.java delete mode 100644 src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java delete mode 100644 src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java delete mode 100644 src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyConfig.java b/src/main/java/org/xbill/DNS/NioSocks5ProxyConfig.java similarity index 57% rename from src/main/java/org/xbill/DNS/Socks5ProxyConfig.java rename to src/main/java/org/xbill/DNS/NioSocks5ProxyConfig.java index 273de4de..e1e24c9a 100644 --- a/src/main/java/org/xbill/DNS/Socks5ProxyConfig.java +++ b/src/main/java/org/xbill/DNS/NioSocks5ProxyConfig.java @@ -1,13 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; +import java.net.InetSocketAddress; import lombok.Getter; import lombok.Setter; -import java.net.InetSocketAddress; - @Getter @Setter -public class Socks5ProxyConfig { +public class NioSocks5ProxyConfig { private InetSocketAddress proxyAddress; private AuthMethod authMethod; private String socks5User; @@ -19,19 +19,20 @@ public enum AuthMethod { USER_PASS } - public Socks5ProxyConfig(InetSocketAddress proxyAddress) { + public NioSocks5ProxyConfig(InetSocketAddress proxyAddress) { this(proxyAddress, null, null); authMethod = AuthMethod.NONE; } - public Socks5ProxyConfig(InetSocketAddress proxyAddress, String socks5User, String socks5Password) { + public NioSocks5ProxyConfig( + InetSocketAddress proxyAddress, String socks5User, String socks5Password) { this.proxyAddress = proxyAddress; this.socks5User = socks5User; this.socks5Password = socks5Password; authMethod = AuthMethod.USER_PASS; } -// public Socks5ProxyConfig(InetSocketAddress proxyAddress, GSSCredential gssCredential) { -// this(proxyAddress, null, null, gssCredential); -// } + // public Socks5ProxyConfig(InetSocketAddress proxyAddress, GSSCredential gssCredential) { + // this(proxyAddress, null, null, gssCredential); + // } } diff --git a/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java b/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java index 48783bfb..035ae080 100644 --- a/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java +++ b/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java @@ -1,17 +1,8 @@ +// SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import java.io.IOException; -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - +import java.util.Objects; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.io.IoClientFactory; import org.xbill.DNS.io.TcpIoClient; @@ -22,20 +13,19 @@ public class NioSocks5ProxyFactory implements IoClientFactory { // SOCKS5 proxy configuration - private final Socks5ProxyConfig config; + private final NioSocks5ProxyConfig config; // io clients private final TcpIoClient tcpIoClient; private final UdpIoClient udpIoClient; // constructor - public NioSocks5ProxyFactory(Socks5ProxyConfig socks5Proxy) { + public NioSocks5ProxyFactory(NioSocks5ProxyConfig socks5Proxy) { config = Objects.requireNonNull(socks5Proxy, "proxy config must not be null"); tcpIoClient = new NioSocksTcpClient(config); udpIoClient = new NioSocksUdpClient(config); } - @Override public TcpIoClient createOrGetTcpClient() { return tcpIoClient; @@ -45,5 +35,4 @@ public TcpIoClient createOrGetTcpClient() { public UdpIoClient createOrGetUdpClient() { return udpIoClient; } - } diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 92782a22..cda4f716 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -1,8 +1,6 @@ +// SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetSocketAddress; @@ -10,6 +8,8 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; @Getter @Slf4j @@ -47,7 +47,12 @@ public class NioSocksHandler { private final String socks5User; private final String socks5Password; - public NioSocksHandler(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress, String socks5User, String socks5Password) { + public NioSocksHandler( + InetSocketAddress proxyAddress, + InetSocketAddress remoteAddress, + InetSocketAddress localAddress, + String socks5User, + String socks5Password) { this.remoteAddress = Objects.requireNonNull(remoteAddress, "remoteAddress must not be null"); this.localAddress = localAddress; this.proxyAddress = Objects.requireNonNull(proxyAddress, "proxyAddress must not be null"); @@ -55,142 +60,190 @@ public NioSocksHandler(InetSocketAddress proxyAddress, InetSocketAddress remoteA this.socks5Password = socks5Password; } - public NioSocksHandler(InetSocketAddress proxyAddress, InetSocketAddress remoteAddress, InetSocketAddress localAddress) { + public NioSocksHandler( + InetSocketAddress proxyAddress, + InetSocketAddress remoteAddress, + InetSocketAddress localAddress) { this(proxyAddress, remoteAddress, localAddress, null, null); } private MethodSelectionRequest getMethodSelectionRequest() { - return new MethodSelectionRequest((this.socks5User != null && this.socks5Password != null) ? SOCKS5_AUTH_USER_PASS : SOCKS5_AUTH_NONE); + return new MethodSelectionRequest( + (this.socks5User != null && this.socks5Password != null) + ? SOCKS5_AUTH_USER_PASS + : SOCKS5_AUTH_NONE); } - public CompletableFuture doAuthHandshake(NioTcpHandler.ChannelState channel, Message query, long endTime) { + public CompletableFuture doAuthHandshake( + NioTcpHandler.ChannelState channel, Message query, long endTime) { CompletableFuture authHandshakeF = new CompletableFuture<>(); CompletableFuture methodSelectionF = new CompletableFuture<>(); MethodSelectionRequest methodSelectionRequest = getMethodSelectionRequest(); - NioTcpHandler.Transaction methodSelectionTransaction = new NioTcpHandler.Transaction(query, methodSelectionRequest.toBytes(), endTime, channel.getChannel(), methodSelectionF); + NioTcpHandler.Transaction methodSelectionTransaction = + new NioTcpHandler.Transaction( + query, + methodSelectionRequest.toBytes(), + endTime, + channel.getChannel(), + methodSelectionF); channel.queueTransaction(methodSelectionTransaction); - methodSelectionF.thenComposeAsync(methodSelectionBytes -> { - if (methodSelectionBytes.length != 2) { - authHandshakeF.completeExceptionally(new UnsupportedOperationException("Invalid SOCKS5 method selection response")); - return null; - } - MethodSelectionResponse methodSelectionResponse = new MethodSelectionResponse(methodSelectionBytes); - if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { - authHandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); - return null; - } - if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_NONE) { - authHandshakeF.complete(null); - } else if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_USER_PASS) { - return handleUserPassAuth(channel, query, endTime, authHandshakeF); - } else if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_GSSAPI) { - // TODO: Implement GSSAPI - authHandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); - } - return null; - }); + methodSelectionF.thenComposeAsync( + methodSelectionBytes -> { + if (methodSelectionBytes.length != 2) { + authHandshakeF.completeExceptionally( + new UnsupportedOperationException("Invalid SOCKS5 method selection response")); + return null; + } + MethodSelectionResponse methodSelectionResponse = + new MethodSelectionResponse(methodSelectionBytes); + if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { + authHandshakeF.completeExceptionally( + new UnsupportedOperationException( + "Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); + return null; + } + if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_NONE) { + authHandshakeF.complete(null); + } else if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_USER_PASS) { + return handleUserPassAuth(channel, query, endTime, authHandshakeF); + } else if (methodSelectionResponse.getMethod() == SOCKS5_AUTH_GSSAPI) { + // TODO: Implement GSSAPI + authHandshakeF.completeExceptionally( + new UnsupportedOperationException( + "Unsupported SOCKS5 method: " + methodSelectionResponse.getMethod())); + } + return null; + }); return authHandshakeF; } - private CompletableFuture handleUserPassAuth(NioTcpHandler.ChannelState channel, Message query, long endTime, CompletableFuture authHandshakeF) { + private CompletableFuture handleUserPassAuth( + NioTcpHandler.ChannelState channel, + Message query, + long endTime, + CompletableFuture authHandshakeF) { CompletableFuture userPassAuthF = new CompletableFuture<>(); UserPassAuthRequest userPassAuthRequest = new UserPassAuthRequest(socks5User, socks5Password); - NioTcpHandler.Transaction userPwdAuthTransaction = new NioTcpHandler.Transaction(query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassAuthF); + NioTcpHandler.Transaction userPwdAuthTransaction = + new NioTcpHandler.Transaction( + query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassAuthF); channel.queueTransaction(userPwdAuthTransaction); - userPassAuthF.thenComposeAsync(authIn -> { - UserPwdAuthResponse userPwdAuthResponse = new UserPwdAuthResponse(authIn); - if (userPwdAuthResponse.getStatus() != SOCKS5_REP_SUCCEEDED) { - authHandshakeF.completeExceptionally(new UnsupportedOperationException("SOCKS5 user/pwd authentication failed with status: " + userPwdAuthResponse.getStatus())); - } else { - authHandshakeF.complete(null); - } - return null; - }); + userPassAuthF.thenComposeAsync( + authIn -> { + UserPwdAuthResponse userPwdAuthResponse = new UserPwdAuthResponse(authIn); + if (userPwdAuthResponse.getStatus() != SOCKS5_REP_SUCCEEDED) { + authHandshakeF.completeExceptionally( + new UnsupportedOperationException( + "SOCKS5 user/pwd authentication failed with status: " + + userPwdAuthResponse.getStatus())); + } else { + authHandshakeF.complete(null); + } + return null; + }); return authHandshakeF; } - public CompletableFuture doSocks5Request(NioTcpHandler.ChannelState channel, byte socks5Cmd, Message query, long endTime) { + public CompletableFuture doSocks5Request( + NioTcpHandler.ChannelState channel, byte socks5Cmd, Message query, long endTime) { CompletableFuture cmdHandshakeF = new CompletableFuture<>(); CompletableFuture commandF = new CompletableFuture<>(); // For CONNECT, DST.ADDR and DST.PORT are the address and port of the destination server. // For UDP ASSOCIATE, DST.ADDR and DST.PORT are the address and port of the UDP client. - // If DST.ADDR and DST.PORT are set to 0.0.0.0:0, the proxy will accept UDP connections from any source address and port. - // After the first packet, the source address and port must not change. If they change, the proxy drops the connection and the UDP association. - InetSocketAddress address = (socks5Cmd == SOCKS5_CMD_CONNECT) ? remoteAddress : new InetSocketAddress("0.0.0.0", 0); + // If DST.ADDR and DST.PORT are set to 0.0.0.0:0, the proxy will accept UDP connections from any + // source address and port. + // After the first packet, the source address and port must not change. If they change, the + // proxy drops the connection and the UDP association. + InetSocketAddress address = + (socks5Cmd == SOCKS5_CMD_CONNECT) ? remoteAddress : new InetSocketAddress("0.0.0.0", 0); CmdRequest cmdRequest = new CmdRequest(socks5Cmd, address); - NioTcpHandler.Transaction commandTransaction = new NioTcpHandler.Transaction(query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); + NioTcpHandler.Transaction commandTransaction = + new NioTcpHandler.Transaction( + query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); channel.queueTransaction(commandTransaction); - commandF.thenComposeAsync(in -> { - CmdResponse cmdResponse = new CmdResponse(in); - if (cmdResponse.getReply() != SOCKS5_REP_SUCCEEDED) { - String errorMessage; - switch (cmdResponse.getReply()) { - case SOCKS5_REP_GENERAL_FAILURE: - errorMessage = "General SOCKS server failure"; - break; - case SOCKS5_REP_CONNECTION_NOT_ALLOWED: - errorMessage = "Connection not allowed by ruleset"; - break; - case SOCKS5_REP_NETWORK_UNREACHABLE: - errorMessage = "Network unreachable"; - break; - case SOCKS5_REP_HOST_UNREACHABLE: - errorMessage = "Host unreachable"; - break; - case SOCKS5_REP_CONNECTION_REFUSED: - errorMessage = "Connection refused by destination host"; - break; - case SOCKS5_REP_TTL_EXPIRED: - errorMessage = "TTL expired"; - break; - case SOCKS5_REP_COMMAND_NOT_SUPPORTED: - errorMessage = "Command not supported"; - break; - case SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED: - errorMessage = "Address type not supported"; - break; - default: - errorMessage = "Unknown SOCKS5 error with status: " + cmdResponse.getReply(); - } - cmdHandshakeF.completeExceptionally(new UnsupportedOperationException("SOCKS5 command failed: " + errorMessage)); - } else { - cmdHandshakeF.complete(in); - } - return null; - }); + commandF.thenComposeAsync( + in -> { + CmdResponse cmdResponse = new CmdResponse(in); + if (cmdResponse.getReply() != SOCKS5_REP_SUCCEEDED) { + String errorMessage; + switch (cmdResponse.getReply()) { + case SOCKS5_REP_GENERAL_FAILURE: + errorMessage = "General SOCKS server failure"; + break; + case SOCKS5_REP_CONNECTION_NOT_ALLOWED: + errorMessage = "Connection not allowed by ruleset"; + break; + case SOCKS5_REP_NETWORK_UNREACHABLE: + errorMessage = "Network unreachable"; + break; + case SOCKS5_REP_HOST_UNREACHABLE: + errorMessage = "Host unreachable"; + break; + case SOCKS5_REP_CONNECTION_REFUSED: + errorMessage = "Connection refused by destination host"; + break; + case SOCKS5_REP_TTL_EXPIRED: + errorMessage = "TTL expired"; + break; + case SOCKS5_REP_COMMAND_NOT_SUPPORTED: + errorMessage = "Command not supported"; + break; + case SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED: + errorMessage = "Address type not supported"; + break; + default: + errorMessage = "Unknown SOCKS5 error with status: " + cmdResponse.getReply(); + } + cmdHandshakeF.completeExceptionally( + new UnsupportedOperationException("SOCKS5 command failed: " + errorMessage)); + } else { + cmdHandshakeF.complete(in); + } + return null; + }); return cmdHandshakeF; } - public synchronized CompletableFuture doSocks5Handshake(NioTcpHandler.ChannelState channel, byte socks5Cmd, Message query, long endTime) { + public synchronized CompletableFuture doSocks5Handshake( + NioTcpHandler.ChannelState channel, byte socks5Cmd, Message query, long endTime) { CompletableFuture socks5HandshakeF = new CompletableFuture<>(); channel.setSocks5(true); CompletableFuture authHandshakeF = doAuthHandshake(channel, query, endTime); - authHandshakeF.thenRunAsync(() -> { - CompletableFuture cmdHandshakeF; - if (socks5Cmd == SOCKS5_CMD_CONNECT || socks5Cmd == SOCKS5_CMD_UDP_ASSOCIATE) { - cmdHandshakeF = doSocks5Request(channel, socks5Cmd, query, endTime); - } else { - socks5HandshakeF.completeExceptionally(new UnsupportedOperationException("Unsupported SOCKS5 command: " + socks5Cmd)); - return; - } - cmdHandshakeF.thenComposeAsync(in -> { - socks5HandshakeF.complete(in); - return null; - }).exceptionally(e -> { - socks5HandshakeF.completeExceptionally(e); - return null; - }); - }).exceptionally(e -> { - socks5HandshakeF.completeExceptionally(e); - return null; - }); + authHandshakeF + .thenRunAsync( + () -> { + CompletableFuture cmdHandshakeF; + if (socks5Cmd == SOCKS5_CMD_CONNECT || socks5Cmd == SOCKS5_CMD_UDP_ASSOCIATE) { + cmdHandshakeF = doSocks5Request(channel, socks5Cmd, query, endTime); + } else { + socks5HandshakeF.completeExceptionally( + new UnsupportedOperationException("Unsupported SOCKS5 command: " + socks5Cmd)); + return; + } + cmdHandshakeF + .thenComposeAsync( + in -> { + socks5HandshakeF.complete(in); + return null; + }) + .exceptionally( + e -> { + socks5HandshakeF.completeExceptionally(e); + return null; + }); + }) + .exceptionally( + e -> { + socks5HandshakeF.completeExceptionally(e); + return null; + }); return socks5HandshakeF; } diff --git a/src/main/java/org/xbill/DNS/NioSocksTcpClient.java b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java index 7fea3a29..cfbdb5f4 100644 --- a/src/main/java/org/xbill/DNS/NioSocksTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java @@ -1,25 +1,21 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.extern.slf4j.Slf4j; -import org.xbill.DNS.io.TcpIoClient; - -import java.io.IOException; import java.net.InetSocketAddress; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; import java.time.Duration; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.TcpIoClient; @Slf4j final class NioSocksTcpClient extends NioTcpHandler implements TcpIoClient { // TCP handler private final NioTcpHandler tcpHandler; // SOCKS5 proxy configuration - private final Socks5ProxyConfig socksConfig; + private final NioSocks5ProxyConfig socksConfig; - NioSocksTcpClient(Socks5ProxyConfig config) { + NioSocksTcpClient(NioSocks5ProxyConfig config) { socksConfig = Objects.requireNonNull(config, "proxy config must not be null"); tcpHandler = new NioTcpHandler(); } @@ -31,7 +27,13 @@ public CompletableFuture sendAndReceiveTcp( Message query, byte[] data, Duration timeout) { - NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local, socksConfig.getSocks5User(), socksConfig.getSocks5Password()); + NioSocksHandler proxy = + new NioSocksHandler( + socksConfig.getProxyAddress(), + remote, + local, + socksConfig.getSocks5User(), + socksConfig.getSocks5Password()); return tcpHandler.sendAndReceiveTcp(local, remote, proxy, query, data, timeout); } } diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java index 060bdb7e..1dca2182 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -1,17 +1,19 @@ +// SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.DatagramChannel; -import java.util.*; +import java.util.Iterator; +import java.util.Map; +import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; @Slf4j public class NioSocksUdpAssociateChannelPool { @@ -25,36 +27,40 @@ public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler u } public SocksUdpAssociateChannelState createOrGetSocketChannelState( - InetSocketAddress local, - InetSocketAddress remote, - NioSocksHandler proxy, - CompletableFuture future) { + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture future) { String key = local + " " + remote; - SocksUdpAssociateChannelGroup group = channelMap.computeIfAbsent(key, - k -> new SocksUdpAssociateChannelGroup(tcpHandler, udpHandler)); + SocksUdpAssociateChannelGroup group = + channelMap.computeIfAbsent( + key, k -> new SocksUdpAssociateChannelGroup(tcpHandler, udpHandler)); return group.createOrGetDatagramChannel(local, remote, proxy, future); } public void removeIdleChannels() { long currentTime = System.currentTimeMillis(); - channelMap.values().forEach(group -> { - for (SocksUdpAssociateChannelState channel : group.channels) { - if (channel.poolChannelIdleTimeout < currentTime) { - try { - group.removeChannelState(channel); - } catch (IOException e) { - log.warn("Error closing idle channel", e); - } - } - } - }); + channelMap + .values() + .forEach( + group -> { + for (SocksUdpAssociateChannelState channel : group.channels) { + if (channel.poolChannelIdleTimeout < currentTime) { + try { + group.removeChannelState(channel); + } catch (IOException e) { + log.warn("Error closing idle channel", e); + } + } + } + }); } private static class SocksUdpAssociateChannelGroup { private final Queue channels; private final NioTcpHandler tcpHandler; private final NioUdpHandler udpHandler; - private final int defaultChannelIdleTimeout = 60000; + private final int defaultChannelIdleTimeout = 1000; public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { channels = new ConcurrentLinkedQueue<>(); @@ -62,11 +68,11 @@ public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udp this.udpHandler = udpHandler; } - public synchronized SocksUdpAssociateChannelState createOrGetDatagramChannel( - InetSocketAddress local, - InetSocketAddress remote, - NioSocksHandler proxy, - CompletableFuture future) { + public SocksUdpAssociateChannelState createOrGetDatagramChannel( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture future) { SocksUdpAssociateChannelState channelState = null; for (Iterator it = channels.iterator(); it.hasNext(); ) { SocksUdpAssociateChannelState c = it.next(); @@ -84,7 +90,8 @@ public synchronized SocksUdpAssociateChannelState createOrGetDatagramChannel( SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); newChannel.tcpChannel = tcpHandler.createChannelState(local, remote, proxy, future); newChannel.udpChannel = udpHandler.createChannel(local, future); - newChannel.poolChannelIdleTimeout = System.currentTimeMillis() + defaultChannelIdleTimeout; + newChannel.poolChannelIdleTimeout = + System.currentTimeMillis() + defaultChannelIdleTimeout; newChannel.isOccupied = true; channels.add(newChannel); channelState = newChannel; diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index 80c81773..f6c4a542 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -1,67 +1,86 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.extern.slf4j.Slf4j; -import org.xbill.DNS.io.UdpIoClient; - import java.net.InetSocketAddress; import java.time.Duration; import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.UdpIoClient; @Slf4j final class NioSocksUdpClient extends NioClient implements UdpIoClient { private final NioUdpHandler udpHandler = new NioUdpHandler(); - private final Socks5ProxyConfig socksConfig; + private final NioSocks5ProxyConfig socksConfig; - NioSocksUdpClient(Socks5ProxyConfig config) { + NioSocksUdpClient(NioSocks5ProxyConfig config) { socksConfig = config; } @Override public CompletableFuture sendAndReceiveUdp( - InetSocketAddress local, - InetSocketAddress remote, - Message query, - byte[] data, - int max, - Duration timeout) { + InetSocketAddress local, + InetSocketAddress remote, + Message query, + byte[] data, + int max, + Duration timeout) { CompletableFuture f = new CompletableFuture<>(); long endTime = System.nanoTime() + timeout.toNanos(); - NioSocksHandler proxy = new NioSocksHandler(socksConfig.getProxyAddress(), remote, local, socksConfig.getSocks5User(), socksConfig.getSocks5Password()); - NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = udpHandler.getUdpPool().createOrGetSocketChannelState(local, remote, proxy, f); + NioSocksHandler proxy = + new NioSocksHandler( + socksConfig.getProxyAddress(), + remote, + local, + socksConfig.getSocks5User(), + socksConfig.getSocks5Password()); + NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = + udpHandler.getUdpPool().createOrGetSocketChannelState(local, remote, proxy, f); - synchronized (channel.getTcpChannel()) { + synchronized (channel) { if (channel.getTcpChannel().socks5HandshakeF == null || channel.getTcpChannel().socks5HandshakeF.isCompletedExceptionally()) { channel.getTcpChannel().setSocks5(true); - channel.getTcpChannel().socks5HandshakeF = proxy.doSocks5Handshake( - channel.getTcpChannel(), NioSocksHandler.SOCKS5_CMD_UDP_ASSOCIATE, query, endTime); + channel.getTcpChannel().socks5HandshakeF = + proxy.doSocks5Handshake( + channel.getTcpChannel(), NioSocksHandler.SOCKS5_CMD_UDP_ASSOCIATE, query, endTime); } } - channel.getTcpChannel().socks5HandshakeF.thenApplyAsync(cmdBytes -> { - NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); - InetSocketAddress newRemote = new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); - byte[] wrappedData = proxy.addUdpHeader(data, newRemote); + channel + .getTcpChannel() + .socks5HandshakeF + .thenApplyAsync( + cmdBytes -> { + NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); + InetSocketAddress newRemote = + new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); + byte[] wrappedData = proxy.addUdpHeader(data, newRemote); - udpHandler.sendAndReceiveUdp(local, newRemote, channel.getUdpChannel(), query, wrappedData, max, timeout) - .thenApplyAsync(response -> { - channel.setOccupied(false); - try { - f.complete(proxy.removeUdpHeader(response)); - } catch (IllegalArgumentException e) { - f.completeExceptionally(e); - } - return null; - }).exceptionally(ex -> { - f.completeExceptionally(ex); - return null; - }); - return null; - }).exceptionally(ex -> { - f.completeExceptionally(ex); - return null; - }); + udpHandler + .sendAndReceiveUdp( + local, newRemote, channel.getUdpChannel(), query, wrappedData, max, timeout) + .thenApplyAsync( + response -> { + channel.setOccupied(false); + try { + f.complete(proxy.removeUdpHeader(response)); + } catch (IllegalArgumentException e) { + f.completeExceptionally(e); + } + return null; + }) + .exceptionally( + ex -> { + f.completeExceptionally(ex); + return null; + }); + return null; + }) + .exceptionally( + ex -> { + f.completeExceptionally(ex); + return null; + }); return f; } diff --git a/src/main/java/org/xbill/DNS/NioTcpClient.java b/src/main/java/org/xbill/DNS/NioTcpClient.java index fa53bbe9..90ed4473 100644 --- a/src/main/java/org/xbill/DNS/NioTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioTcpClient.java @@ -4,7 +4,6 @@ import java.net.InetSocketAddress; import java.time.Duration; import java.util.concurrent.CompletableFuture; - import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.io.TcpIoClient; diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 362caf3e..62d4b0be 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -1,12 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; @@ -22,12 +16,18 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; @Slf4j @Getter public class NioTcpHandler extends NioClient { // registrationQueue and channelMap must be static to be shared between instances - // otherwise, a second instance would overwrite the registration, timeout and close tasks of the first instance + // otherwise, a second instance would overwrite the registration, timeout and close tasks of the + // first instance private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); private static final Map channelMap = new ConcurrentHashMap<>(); @@ -180,10 +180,10 @@ public void close() { channel.close(); } catch (IOException ex) { log.warn( - "Failed to close channel l={}/r={}", - entry.getKey().local, - entry.getKey().remote, - ex); + "Failed to close channel l={}/r={}", + entry.getKey().local, + entry.getKey().remote, + ex); } return; } @@ -210,7 +210,8 @@ private void processRead() { } responseData.flip(); byte[] data = new byte[responseData.limit()]; - System.arraycopy(responseData.array(), responseData.arrayOffset(), data, 0, responseData.limit()); + System.arraycopy( + responseData.array(), responseData.arrayOffset(), data, 0, responseData.limit()); // the transactions for the socks5 handshake are synchronized // you can assume that the responses are in order of the transactions in the queue for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { @@ -324,7 +325,8 @@ static class ChannelKey { final InetSocketAddress remote; } - public void dnsTransaction(ChannelState channel, Message query, byte[] data, long endTime, CompletableFuture f) { + public void dnsTransaction( + ChannelState channel, Message query, byte[] data, long endTime, CompletableFuture f) { // Transaction for the main data channel.setSocks5(false); // combine length+message to avoid multiple TCP packets @@ -337,7 +339,11 @@ public void dnsTransaction(ChannelState channel, Message query, byte[] data, lon channel.queueTransaction(t); } - public ChannelState createChannelState(InetSocketAddress local, InetSocketAddress remote, NioSocksHandler proxy, CompletableFuture f) { + public ChannelState createChannelState( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture f) { log.debug("Opening async channel for l={}/r={}", local, remote); SocketChannel c = null; try { @@ -366,47 +372,55 @@ public ChannelState createChannelState(InetSocketAddress local, InetSocketAddres } } - public ChannelState createOrGetChannelState(InetSocketAddress local, InetSocketAddress remote, NioSocksHandler proxy, CompletableFuture f) { + public ChannelState createOrGetChannelState( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + CompletableFuture f) { return channelMap.computeIfAbsent( - new ChannelKey(local, remote), - key -> createChannelState(local, remote, proxy, f) - ); + new ChannelKey(local, remote), key -> createChannelState(local, remote, proxy, f)); } public CompletableFuture sendAndReceiveTcp( - InetSocketAddress local, - InetSocketAddress remote, - NioSocksHandler proxy, - Message query, - byte[] data, - Duration timeout) { + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + Message query, + byte[] data, + Duration timeout) { CompletableFuture f = new CompletableFuture<>(); ChannelState channel = createOrGetChannelState(local, remote, proxy, f); if (channel != null) { log.trace( - "Creating transaction for id {} ({}/{})", - query.getHeader().getID(), - query.getQuestion().getName(), - Type.string(query.getQuestion().getType())); + "Creating transaction for id {} ({}/{})", + query.getHeader().getID(), + query.getQuestion().getName(), + Type.string(query.getQuestion().getType())); long endTime = System.nanoTime() + timeout.toNanos(); if (proxy != null) { synchronized (channel) { - if (channel.socks5HandshakeF == null || channel.socks5HandshakeF.isCompletedExceptionally()) { + if (channel.socks5HandshakeF == null + || channel.socks5HandshakeF.isCompletedExceptionally()) { channel.setSocks5(true); - channel.socks5HandshakeF = proxy.doSocks5Handshake(channel, NioSocksHandler.SOCKS5_CMD_CONNECT, query, endTime); + channel.socks5HandshakeF = + proxy.doSocks5Handshake( + channel, NioSocksHandler.SOCKS5_CMD_CONNECT, query, endTime); } } // Chain the SOCKS5 transactions with the TCP data transaction - channel.socks5HandshakeF.thenRunAsync( - () -> { - dnsTransaction(channel, query, data, endTime, f); - } - ).exceptionally(ex -> { - f.completeExceptionally(ex); - return null; - }); + channel + .socks5HandshakeF + .thenRunAsync( + () -> { + dnsTransaction(channel, query, data, endTime, f); + }) + .exceptionally( + ex -> { + f.completeExceptionally(ex); + return null; + }); } else { // transaction for DNS data dnsTransaction(channel, query, data, endTime, f); @@ -415,5 +429,4 @@ public CompletableFuture sendAndReceiveTcp( return f; } - } diff --git a/src/main/java/org/xbill/DNS/NioUdpClient.java b/src/main/java/org/xbill/DNS/NioUdpClient.java index c163634a..87d28617 100644 --- a/src/main/java/org/xbill/DNS/NioUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioUdpClient.java @@ -1,22 +1,9 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import java.io.EOFException; -import java.io.IOException; import java.net.InetSocketAddress; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.security.SecureRandom; import java.time.Duration; -import java.util.Iterator; -import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.io.UdpIoClient; @@ -38,5 +25,4 @@ public CompletableFuture sendAndReceiveUdp( Duration timeout) { return udpHandler.sendAndReceiveUdp(local, remote, null, query, data, max, timeout); } - } diff --git a/src/main/java/org/xbill/DNS/NioUdpHandler.java b/src/main/java/org/xbill/DNS/NioUdpHandler.java index 5ffe2234..332eb990 100644 --- a/src/main/java/org/xbill/DNS/NioUdpHandler.java +++ b/src/main/java/org/xbill/DNS/NioUdpHandler.java @@ -1,10 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; @@ -14,13 +10,15 @@ import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; import java.security.SecureRandom; import java.time.Duration; -import java.util.*; +import java.util.Iterator; +import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Slf4j @Getter @@ -167,8 +165,10 @@ public void processReadyKey(SelectionKey key) { // do not close the channel in case of SOCKS5 UDP associate. // the channel port needs to be claimed for further queries to the same remote host. // you can not use the same UDP associate port with another local port after the first query. - // you can also close this channel and open a new one with the same local port for further queries, - // but I would like to avoid, that the local port will be taken by another process between queries. + // you can also close this channel and open a new one with the same local port for further + // queries, + // but I would like to avoid, that the local port will be taken by another process between + // queries. if (!isProxyChannel) { silentDisconnectAndCloseChannel(); } @@ -192,35 +192,35 @@ private void silentDisconnectAndCloseChannel() { } } - - public DatagramChannel createChannel(InetSocketAddress local, CompletableFuture f) throws IOException { + public DatagramChannel createChannel(InetSocketAddress local, CompletableFuture f) + throws IOException { DatagramChannel channel = DatagramChannel.open(); channel.configureBlocking(false); if (local == null || local.getPort() == 0) { boolean bound = false; - for (int i = 0; i < 1024; i++) { - try { - InetSocketAddress addr = null; - if (local == null) { - if (prng != null) { - addr = new InetSocketAddress(prng.nextInt(ephemeralRange) + ephemeralStart); - } - } else { - int port = local.getPort(); - if (port == 0 && prng != null) { - port = prng.nextInt(ephemeralRange) + ephemeralStart; - } - - addr = new InetSocketAddress(local.getAddress(), port); + for (int i = 0; i < 1024; i++) { + try { + InetSocketAddress addr = null; + if (local == null) { + if (prng != null) { + addr = new InetSocketAddress(prng.nextInt(ephemeralRange) + ephemeralStart); + } + } else { + int port = local.getPort(); + if (port == 0 && prng != null) { + port = prng.nextInt(ephemeralRange) + ephemeralStart; } - channel.bind(addr); - bound = true; - break; - } catch (SocketException e) { - // ignore, we'll try another random port + addr = new InetSocketAddress(local.getAddress(), port); } + + channel.bind(addr); + bound = true; + break; + } catch (SocketException e) { + // ignore, we'll try another random port } + } if (!bound) { f.completeExceptionally(new IOException("No available source port found")); return null; @@ -256,7 +256,9 @@ public CompletableFuture sendAndReceiveUdp( return f; } - Transaction t = new Transaction(query.getHeader().getID(), data, max, endTime, channel, isProxyChannel, f); + Transaction t = + new Transaction( + query.getHeader().getID(), data, max, endTime, channel, isProxyChannel, f); final Selector selector = selector(); pendingTransactions.add(t); diff --git a/src/main/java/org/xbill/DNS/Socks5Proxy.java b/src/main/java/org/xbill/DNS/Socks5Proxy.java deleted file mode 100644 index 0899b0a0..00000000 --- a/src/main/java/org/xbill/DNS/Socks5Proxy.java +++ /dev/null @@ -1,432 +0,0 @@ -package org.xbill.DNS; - -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.CompletableFuture; - -@Slf4j -@Getter -@Setter -public class Socks5Proxy { - private static final byte SOCKS5_VERSION = 0x05; - private static final byte SOCKS5_USER_PWD_AUTH_VERSION = 0x01; - private static final byte SOCKS5_AUTH_NONE = 0x00; - private static final byte SOCKS5_AUTH_GSSAPI = 0x01; - private static final byte SOCKS5_AUTH_USER_PASS = 0x02; - private static final byte SOCKS5_AUTH_NO_ACCEPTABLE_METHODS = (byte) 0xFF; - - private static final byte SOCKS5_CMD_CONNECT = 0x01; - private static final byte SOCKS5_CMD_BIND = 0x02; - private static final byte SOCKS5_CMD_UDP_ASSOCIATE = 0x03; - - private static final byte SOCKS5_ATYP_IPV4 = 0x01; - private static final byte SOCKS5_ATYP_DOMAINNAME = 0x03; - private static final byte SOCKS5_ATYP_IPV6 = 0x04; - - private static final byte SOCKS5_REP_SUCCEEDED = 0x00; - private static final byte SOCKS5_REP_GENERAL_FAILURE = 0x01; - private static final byte SOCKS5_REP_CONNECTION_NOT_ALLOWED = 0x02; - private static final byte SOCKS5_REP_NETWORK_UNREACHABLE = 0x03; - private static final byte SOCKS5_REP_HOST_UNREACHABLE = 0x04; - private static final byte SOCKS5_REP_CONNECTION_REFUSED = 0x05; - private static final byte SOCKS5_REP_TTL_EXPIRED = 0x06; - private static final byte SOCKS5_REP_COMMAND_NOT_SUPPORTED = 0x07; - private static final byte SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED = 0x08; - - private static final byte SOCKS5_RESERVED = 0x00; - - public enum State { - INIT, - UDP_ASSOCIATE, - CONNECTED, - FAILED - } - - public enum Command { - CONNECT, - UDP_ASSOCIATE - } - - private final Socks5ProxyConfig config; - private final InetSocketAddress localAddress; - private final InetSocketAddress proxyAddress; - private final InetSocketAddress remoteAddress; - private SelectionKey tcpSelectionKey; - private DatagramChannel udpChannel; - private final Command command; - private State state = State.INIT; - - public Socks5Proxy( - SelectionKey tcpSelectionKey, - Socks5ProxyConfig config, - InetSocketAddress local, - InetSocketAddress proxyAddress, - InetSocketAddress remote, - Command command) { - this.tcpSelectionKey = tcpSelectionKey; - this.config = config; - this.localAddress = local; - this.proxyAddress = proxyAddress; - this.remoteAddress = remote; - this.command = command; - } - - public void handleSOCKS5(CompletableFuture future) { - tcpSelectionKey.attach(new ConnectHandler(future)); - tcpSelectionKey.interestOps(SelectionKey.OP_CONNECT); - tcpSelectionKey.selector().wakeup(); - } - - public ByteBuffer addSocks5UdpAssociateHeader(byte[] data) { - ByteBuffer buffer; - byte addressType; - byte[] addressBytes; - if (remoteAddress.getAddress() instanceof Inet4Address) { - addressType = SOCKS5_ATYP_IPV4; - addressBytes = remoteAddress.getAddress().getAddress(); - buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); - } else if (remoteAddress.getAddress() instanceof Inet6Address) { - addressType = SOCKS5_ATYP_IPV6; - addressBytes = remoteAddress.getAddress().getAddress(); - buffer = ByteBuffer.allocate(4 + addressBytes.length + 2 + data.length); - } else { - addressType = SOCKS5_ATYP_DOMAINNAME; - addressBytes = remoteAddress.getHostName().getBytes(StandardCharsets.UTF_8); - buffer = ByteBuffer.allocate(4 + 1 + addressBytes.length + 2 + data.length); - } - - buffer.put((byte) 0x00); // RSV - buffer.put((byte) 0x00); // RSV - buffer.put((byte) 0x00); // FRAG - buffer.put(addressType); // ATYP (IPv4) - if (addressType == SOCKS5_ATYP_DOMAINNAME) { - buffer.put((byte) addressBytes.length); - } - buffer.put(addressBytes); // DST.ADDR - buffer.putShort((short) remoteAddress.getPort()); // DST.PORT - buffer.put(data); // DATA - - return buffer; - } - - private class ConnectHandler implements Runnable { - private final CompletableFuture future; - - ConnectHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - if (channel.finishConnect()) { - // Connection finished successfully - tcpSelectionKey.attach(new Socks5MethodSelectionHandler(future)); - tcpSelectionKey.interestOps(SelectionKey.OP_WRITE); - } else { - // Connection not finished, re-register for OP_CONNECT - tcpSelectionKey.interestOps(SelectionKey.OP_CONNECT); - } - tcpSelectionKey.selector().wakeup(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } - - private class Socks5MethodSelectionHandler implements Runnable { - private final CompletableFuture future; - - Socks5MethodSelectionHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - ByteBuffer buffer = ByteBuffer.allocate(3); - buffer.put(SOCKS5_VERSION); - buffer.put((byte) 1); - buffer.put((config.getAuthMethod() == Socks5ProxyConfig.AuthMethod.USER_PASS) ? SOCKS5_AUTH_USER_PASS : SOCKS5_AUTH_NONE); - buffer.flip(); - channel.write(buffer); - - tcpSelectionKey.attach(new Socks5MethodSelectionReadHandler(future)); - tcpSelectionKey.interestOps(SelectionKey.OP_READ); - tcpSelectionKey.selector().wakeup(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } - - private class Socks5MethodSelectionReadHandler implements Runnable { - private final CompletableFuture future; - - Socks5MethodSelectionReadHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - ByteBuffer buffer = ByteBuffer.allocate(2); - channel.read(buffer); - buffer.flip(); - - if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalStateException("Invalid SOCKS5 version"); - } - - byte method = buffer.get(); - if (method == SOCKS5_AUTH_NO_ACCEPTABLE_METHODS) { - throw new IllegalStateException("No acceptable authentication methods"); - } - - if (method == SOCKS5_AUTH_USER_PASS) { - tcpSelectionKey.attach(new Socks5UserPassAuthHandler(future)); - } else { - if (command == Command.CONNECT) { - tcpSelectionKey.attach(new Socks5ConnectExchangeHandler(future)); - } else if (command == Command.UDP_ASSOCIATE) { - tcpSelectionKey.attach(new Socks5UdpAssociateExchangeHandler(future)); - } else { - throw new IllegalStateException("Unsupported command: " + command); - } - } - - tcpSelectionKey.interestOps(SelectionKey.OP_WRITE); - tcpSelectionKey.selector().wakeup(); - } catch (IOException | IllegalStateException e) { - future.completeExceptionally(e); - } - } - } - - private class Socks5UserPassAuthHandler implements Runnable { - private final CompletableFuture future; - - Socks5UserPassAuthHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - ByteBuffer buffer = ByteBuffer.allocate(2 + config.getSocks5User().length() + 2 + config.getSocks5Password().length()); - buffer.put(SOCKS5_USER_PWD_AUTH_VERSION); - buffer.put((byte) config.getSocks5User().length()); - buffer.put(config.getSocks5User().getBytes()); - buffer.put((byte) config.getSocks5Password().length()); - buffer.put(config.getSocks5Password().getBytes()); - buffer.flip(); - channel.write(buffer); - - tcpSelectionKey.attach(new Socks5UserPassAuthReadHandler(future)); - tcpSelectionKey.interestOps(SelectionKey.OP_READ); - tcpSelectionKey.selector().wakeup(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } - - private class Socks5UserPassAuthReadHandler implements Runnable { - private final CompletableFuture future; - - Socks5UserPassAuthReadHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - ByteBuffer buffer = ByteBuffer.allocate(2); - channel.read(buffer); - buffer.flip(); - - if (buffer.get() != SOCKS5_USER_PWD_AUTH_VERSION) { - throw new IllegalStateException("Invalid SOCKS5 user/password auth version"); - } - - byte status = buffer.get(); - if (status != 0x00) { - throw new IllegalStateException("User/password authentication failed"); - } - - if (command == Command.CONNECT) { - tcpSelectionKey.attach(new Socks5ConnectExchangeHandler(future)); - } else if (command == Command.UDP_ASSOCIATE) { - tcpSelectionKey.attach(new Socks5UdpAssociateExchangeHandler(future)); - } else { - throw new IllegalStateException("Unsupported command: " + command); - } - tcpSelectionKey.interestOps(SelectionKey.OP_WRITE); - tcpSelectionKey.selector().wakeup(); - } catch (IOException | IllegalStateException e) { - future.completeExceptionally(e); - } - } - } - - private class Socks5ConnectExchangeHandler implements Runnable { - private final CompletableFuture future; - - Socks5ConnectExchangeHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - ByteBuffer buffer; - byte addressType; - byte[] addressBytes; - - if (remoteAddress.getAddress() instanceof Inet4Address) { - addressType = SOCKS5_ATYP_IPV4; - addressBytes = remoteAddress.getAddress().getAddress(); - buffer = ByteBuffer.allocate(10); - } else if (remoteAddress.getAddress() instanceof Inet6Address) { - addressType = SOCKS5_ATYP_IPV6; - addressBytes = remoteAddress.getAddress().getAddress(); - buffer = ByteBuffer.allocate(22); - } else { - addressType = SOCKS5_ATYP_DOMAINNAME; - addressBytes = remoteAddress.getHostName().getBytes(StandardCharsets.UTF_8); - buffer = ByteBuffer.allocate(7 + addressBytes.length); - } - - buffer.put(SOCKS5_VERSION); - buffer.put(SOCKS5_CMD_CONNECT); - buffer.put(SOCKS5_RESERVED); - buffer.put(addressType); - if (addressType == SOCKS5_ATYP_DOMAINNAME) { - buffer.put((byte) addressBytes.length); - } - buffer.put(addressBytes); - buffer.putShort((short) remoteAddress.getPort()); - buffer.flip(); - channel.write(buffer); - - tcpSelectionKey.attach(new Socks5HeaderExchangeReadHandler(future)); - tcpSelectionKey.interestOps(SelectionKey.OP_READ); - tcpSelectionKey.selector().wakeup(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } - - private class Socks5UdpAssociateExchangeHandler implements Runnable { - private final CompletableFuture future; - - Socks5UdpAssociateExchangeHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - ByteBuffer buffer = ByteBuffer.allocate(10); - buffer.put(SOCKS5_VERSION); - buffer.put(SOCKS5_CMD_UDP_ASSOCIATE); - buffer.put(SOCKS5_RESERVED); - // For UDP associate this is not the remote address, - // but the address where the proxy will send UDP packets after receiving them from the remote address - // there is a header for the remote address in the UDP packet - buffer.put(SOCKS5_ATYP_IPV4); - buffer.putInt(0); // 0.0.0.0 (this way it works in nat-ed networks) - buffer.putShort((short) 0); // Port 0 - buffer.flip(); - channel.write(buffer); - - tcpSelectionKey.attach(new Socks5HeaderExchangeReadHandler(future)); - tcpSelectionKey.interestOps(SelectionKey.OP_READ); - tcpSelectionKey.selector().wakeup(); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } - - private class Socks5HeaderExchangeReadHandler implements Runnable { - private final CompletableFuture future; - - Socks5HeaderExchangeReadHandler(CompletableFuture future) { - this.future = future; - } - - @Override - public void run() { - try { - SocketChannel channel = (SocketChannel) tcpSelectionKey.channel(); - // Allocate 262 bytes to handle the maximum possible size of the SOCKS5 reply - ByteBuffer buffer = ByteBuffer.allocate(262); - channel.read(buffer); - buffer.flip(); - - if (buffer.get() != SOCKS5_VERSION) { - throw new IllegalStateException("Invalid SOCKS5 version"); - } - - byte reply = buffer.get(); - if (reply != SOCKS5_REP_SUCCEEDED) { - throw new IllegalStateException("Connection to remote server failed: " + reply); - } - - if (command == Command.CONNECT) { - state = State.CONNECTED; - // ignore rest of the reply - } else { - state = State.UDP_ASSOCIATE; - // get the bound port for UDP associate - /// skip reserved byte - buffer.get(); - /// read the bound address and port - byte addressType = buffer.get(); - byte[] boundAddress; - if (addressType == SOCKS5_ATYP_IPV4) { - boundAddress = new byte[4]; - } else if (addressType == SOCKS5_ATYP_IPV6) { - boundAddress = new byte[16]; - } else if (addressType == SOCKS5_ATYP_DOMAINNAME) { - int domainLength = buffer.get(); - boundAddress = new byte[domainLength]; - } else { - throw new IllegalStateException("Unsupported address type: " + addressType); - } - buffer.get(boundAddress); - // Short.toUnsignedInt makes a difference for port numbers higher than 32767 - int udpAssociatePort = Short.toUnsignedInt(buffer.getShort()); - udpChannel = DatagramChannel.open(); - udpChannel.configureBlocking(false); - udpChannel.bind(new InetSocketAddress(localAddress.getAddress(), 0)); - udpChannel.connect(new InetSocketAddress(config.getProxyAddress().getAddress(), udpAssociatePort)); - } - - future.complete(null); - } catch (IOException e) { - future.completeExceptionally(e); - } - } - } -} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java b/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java deleted file mode 100644 index b871cc41..00000000 --- a/src/main/java/org/xbill/DNS/Socks5ProxyIoClientFactory.java +++ /dev/null @@ -1,236 +0,0 @@ -//package org.xbill.DNS; -// -//import java.io.IOException; -//import java.nio.channels.SelectableChannel; -//import java.nio.channels.SelectionKey; -//import java.nio.channels.Selector; -//import java.util.*; -//import java.util.concurrent.ConcurrentHashMap; -//import java.util.concurrent.Executors; -//import java.util.concurrent.ScheduledExecutorService; -//import java.util.concurrent.TimeUnit; -// -//import lombok.Getter; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.xbill.DNS.io.IoClientFactory; -//import org.xbill.DNS.io.TcpIoClient; -//import org.xbill.DNS.io.UdpIoClient; -// -//@Getter -//@Slf4j -//public class Socks5ProxyIoClientFactory implements IoClientFactory { -// -// // SOCKS5 proxy configuration -// private final Socks5ProxyConfig config; -// -// // connection pool Socks5ProxyConnection -// private static final Map> socks5ConnectionPool = new ConcurrentHashMap<>(); -// -// // selector for handling IO events -// private volatile Selector selector; -// private Thread eventLoopThread; -// private volatile boolean eventLoopRunning = false; -// -// // scheduler for handling timeouts, cleanup and closing connections -// private ScheduledExecutorService timeoutScheduler; -// private static final long timeout = 30000; // 30 seconds timeout -// private final Map keyTimestamps = new ConcurrentHashMap<>(); -// -// // constructor -// public Socks5ProxyIoClientFactory(Socks5ProxyConfig socks5Proxy) { -// config = Objects.requireNonNull(socks5Proxy, "proxy config must not be null"); -// -// // start event loop if not already running -// startEventLoop(); -// -// // Add shutdown hook for graceful shutdown -// Runtime.getRuntime().addShutdownHook(new Thread(this::stopEventLoop)); -// } -// -// // method to start the event loop -// private synchronized void startEventLoop() { -// try { -// selector = Selector.open(); -// } catch (IOException e) { -// log.error("Error opening selector", e); -// return; -// } -// -// eventLoopRunning = true; -// timeoutScheduler = Executors.newScheduledThreadPool(1); -// eventLoopThread = new Thread(() -> { -// try { -// while (eventLoopRunning) { -// // blocking call, waits for an io event -// selector.select(); -// -// // get the set of keys with pending events -// Set selectedKeys = selector.selectedKeys(); -// Iterator keyIterator = selectedKeys.iterator(); -// while (keyIterator.hasNext()) { -// SelectionKey key = keyIterator.next(); -// keyIterator.remove(); -// if (key.isValid()) { -// // run the task associated with the key -// ((Runnable) key.attachment()).run(); -// // update the timestamp for the key -//// long currentTime = System.currentTimeMillis(); -//// keyTimestamps.put(key, currentTime); -//// scheduleTimeout(selector, key); -// } -// } -// } -// } catch (IOException e) { -// log.error("Error in event loop", e); -// } finally { -// try { -// if (selector != null) { -// selector.close(); -// } -// } catch (IOException e) { -// log.error("Error closing selector", e); -// } -// eventLoopRunning = false; -// } -// }); -// eventLoopThread.start(); -// } -// -// private void scheduleTimeout(Selector selector, SelectionKey key) { -// timeoutScheduler.schedule(() -> { -// long currentTime = System.currentTimeMillis(); -// if (currentTime - keyTimestamps.getOrDefault(key, 0L) > timeout) { -// log.debug("Closing connection due to timeout"); -// try { -// key.cancel(); -// key.channel().close(); -// } catch (IOException e) { -// log.error("Error closing channel due to timeout", e); -// } -// keyTimestamps.remove(key); -// selector.wakeup(); -// } -// }, timeout, TimeUnit.MILLISECONDS); -// } -// -// // graceful shutdown of the event loop -// public void stopEventLoop() { -// // stop the event loop -// eventLoopRunning = false; -// if (eventLoopThread != null) { -// eventLoopThread.interrupt(); -// } -// // stop the timeout scheduler -// if (timeoutScheduler != null) { -// timeoutScheduler.shutdownNow(); -// } -// // close all connections in the pool -// for (Map subConnections : socks5ConnectionPool.values()) { -// for (Socks5Proxy connection : subConnections.values()) { -// try { -// connection.getTcpSelectionKey().channel().close(); -// connection.getTcpSelectionKey().cancel(); -// } catch (IOException e) { -// log.error("Error closing connection", e); -// } -// } -// } -// // close the selector -// try { -// selector.close(); -// } catch (IOException e) { -// log.error("Error closing selector", e); -// } -// socks5ConnectionPool.clear(); -// } -// -// // check if the event loop thread is alive for health checks -// public boolean isEventLoopThreadAlive() { -// return eventLoopThread != null && eventLoopThread.isAlive(); -// } -// -// // check if the timeout scheduler is running for health checks -// public boolean isTimeoutSchedulerRunning() { -// return timeoutScheduler != null && !timeoutScheduler.isShutdown(); -// } -// -// // check if the event loop is healthy overall -// public boolean isEventLoopHealthy() { -// return isEventLoopThreadAlive() && isTimeoutSchedulerRunning(); -// } -// -// // register a new connection to the selector -// public SelectionKey registerToSelector(SelectableChannel conn) throws IOException { -// return conn.register(selector, SelectionKey.OP_CONNECT); -// } -// -// // unregister a connection from the selector -// public synchronized void unregisterFromSelector( -// PoolConn poolConn, -// Throwable ex) throws IOException { -// if ( -// ex == null -// && poolConn.getSocks5Conn().getTcpSelectionKey().isValid() -// && poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen() -// ) { -// // unregister for reuse -// poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); -// } else { -// // clean up the socks connection instance in case of an exception or invalid state -// cleanupConnectionFromPool(poolConn); -// } -// } -// -// public synchronized PoolConn getPoolConnFromPool(String connectionID) { -// Map subConnections = socks5ConnectionPool.get(connectionID); -// if (subConnections != null && !subConnections.isEmpty()) { -// for (Map.Entry entry : subConnections.entrySet()) { -// Socks5Proxy socks5Conn = entry.getValue(); -// if (socks5Conn.getTcpSelectionKey().channel().isOpen() -// && !socks5Conn.getTcpSelectionKey().channel().isRegistered()) { -// return new PoolConn(connectionID, entry.getKey(), socks5Conn); -// } -// } -// } -// return null; -// } -// -// @Getter -// @RequiredArgsConstructor -// public static class PoolConn { -// private final String connectionID; -// private final String subConnectionID; -// private final Socks5Proxy socks5Conn; -// } -// -// public synchronized PoolConn addConnectionToPool(String connectionID, Socks5Proxy socks5Conn) { -// String subConnectionID = UUID.randomUUID().toString(); -// socks5ConnectionPool.computeIfAbsent(connectionID, k -> new ConcurrentHashMap<>()).put(subConnectionID, socks5Conn); -// return new PoolConn(connectionID, subConnectionID, socks5Conn); -// } -// -// public synchronized void cleanupConnectionFromPool(PoolConn poolConn) throws IOException { -// if (poolConn.getSocks5Conn() != null && poolConn.getSocks5Conn().getTcpSelectionKey() != null) { -// poolConn.getSocks5Conn().getTcpSelectionKey().channel().close(); -// poolConn.getSocks5Conn().getTcpSelectionKey().cancel(); -// } -// Map subConnections = socks5ConnectionPool.get(poolConn.getConnectionID()); -// if (subConnections != null) { -// subConnections.remove(poolConn.getSubConnectionID()); -// if (subConnections.isEmpty()) { -// socks5ConnectionPool.remove(poolConn.getConnectionID()); -// } -// } -// } -// -// @Override -// public TcpIoClient createOrGetTcpClient() { -// return new Socks5ProxyTcpIoClient(this, config); -// } -// -// @Override -// public UdpIoClient createOrGetUdpClient() { -// return new Socks5ProxyUdpIoClient(this, config); -// } -//} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java b/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java deleted file mode 100644 index f81fd5d4..00000000 --- a/src/main/java/org/xbill/DNS/Socks5ProxyTcpIoClient.java +++ /dev/null @@ -1,156 +0,0 @@ -//package org.xbill.DNS; -// -//import lombok.extern.slf4j.Slf4j; -//import org.xbill.DNS.io.TcpIoClient; -// -//import java.io.IOException; -//import java.net.InetSocketAddress; -//import java.nio.ByteBuffer; -//import java.nio.channels.SelectionKey; -//import java.nio.channels.SocketChannel; -//import java.time.Duration; -//import java.util.concurrent.CompletableFuture; -// -//@Slf4j -//public class Socks5ProxyTcpIoClient implements TcpIoClient { -// private final Socks5ProxyIoClientFactory factory; -// private final Socks5ProxyConfig config; -// private Socks5ProxyIoClientFactory.PoolConn poolConn; -// -// public Socks5ProxyTcpIoClient(Socks5ProxyIoClientFactory factory, Socks5ProxyConfig config) { -// this.factory = factory; -// this.config = config; -// } -// -// public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { -// Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); -// try { -// if (poolConn == null || !poolConn.getSocks5Conn().getTcpSelectionKey().channel().isOpen()) { -// SocketChannel tcpConn = SocketChannel.open(); -// tcpConn.configureBlocking(false); -// SelectionKey selectionKey = factory.registerToSelector(tcpConn); -// Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.CONNECT); -// tcpConn.connect(config.getProxyAddress()); -// socks5Conn.handleSOCKS5(f); -// this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); -// } else { -// SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); -// poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); -// this.poolConn = poolConn; -// f.complete(null); -// } -// } catch (IOException e) { -// f.completeExceptionally(e); -// } -// } -// -// @Override -// public CompletableFuture sendAndReceiveTcp( -// InetSocketAddress local, -// InetSocketAddress remote, -// Message query, -// byte[] data, -// Duration timeout) { -// if (local == null) { -// local = new InetSocketAddress(0); -// } -// // keyString is used to identify and reuse SOCKS5 connections -// String keyString = local.toString() + "-" + remote.toString() + "-TCP"; -// CompletableFuture socksF = new CompletableFuture<>(); -// this.initOrReuseConn(socksF, keyString, local, remote); -// -// return socksF.thenComposeAsync(v -> { -// CompletableFuture dataF = new CompletableFuture<>(); -// try { -// poolConn.getSocks5Conn().getTcpSelectionKey().attach(new SendHandler(dataF, data, poolConn.getSocks5Conn().getTcpSelectionKey())); -// poolConn.getSocks5Conn().getTcpSelectionKey().interestOps(SelectionKey.OP_WRITE); -// poolConn.getSocks5Conn().getTcpSelectionKey().selector().wakeup(); -// } catch (Exception e) { -// dataF.completeExceptionally(e); -// } -// return dataF; -// }).whenComplete((result, ex) -> { -// try { -// factory.unregisterFromSelector(poolConn, ex); -// } catch (IOException e) { -// throw new RuntimeException(e); -// } -// }); -// } -// -// private class SendHandler implements Runnable { -// private final CompletableFuture future; -// private final byte[] data; -// private final SelectionKey selectionKey; -// -// public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { -// this.future = future; -// this.data = data; -// this.selectionKey = selectionKey; -// } -// -// @Override -// public void run() { -// try { -// SocketChannel channel = (SocketChannel) selectionKey.channel(); -// ByteBuffer buffer = ByteBuffer.allocate(data.length + 2); -// buffer.put((byte) (data.length >>> 8)); -// buffer.put((byte) (data.length & 0xFF)); -// buffer.put(data); -// buffer.flip(); -// while (buffer.hasRemaining()) { -// channel.write(buffer); -// } -// selectionKey.attach(new ReceiveHandler(future, selectionKey)); -// selectionKey.interestOps(SelectionKey.OP_READ); -// selectionKey.selector().wakeup(); -// } catch (IOException e) { -// future.completeExceptionally(e); -// } -// } -// } -// -// private class ReceiveHandler implements Runnable { -// private final CompletableFuture future; -// private final SelectionKey selectionKey; -// private final ByteBuffer responseLengthData = ByteBuffer.allocate(2); -// private ByteBuffer responseData; -// -// public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey) { -// this.future = future; -// this.selectionKey = selectionKey; -// } -// -// @Override -// public void run() { -// try { -// SocketChannel channel = (SocketChannel) selectionKey.channel(); -// if (responseData == null) { -// int read = channel.read(responseLengthData); -// if (read < 0) { -// throw new IOException("Connection closed by peer"); -// } -// if (responseLengthData.position() == 2) { -// responseLengthData.flip(); -// int length = ((responseLengthData.get(0) & 0xFF) << 8) + (responseLengthData.get(1) & 0xFF); -// responseData = ByteBuffer.allocate(length); -// } -// } -// if (responseData != null) { -// int read = channel.read(responseData); -// if (read < 0) { -// throw new IOException("Connection closed by peer"); -// } -// if (!responseData.hasRemaining()) { -// responseData.flip(); -// byte[] data = new byte[responseData.limit()]; -// responseData.get(data); -// future.complete(data); -// } -// } -// } catch (IOException e) { -// future.completeExceptionally(e); -// } -// } -// } -//} diff --git a/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java b/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java deleted file mode 100644 index a692ec5a..00000000 --- a/src/main/java/org/xbill/DNS/Socks5ProxyUdpIoClient.java +++ /dev/null @@ -1,158 +0,0 @@ -//package org.xbill.DNS; -// -//import org.xbill.DNS.io.UdpIoClient; -// -//import java.io.EOFException; -//import java.io.IOException; -//import java.net.InetSocketAddress; -//import java.nio.ByteBuffer; -//import java.nio.channels.DatagramChannel; -//import java.nio.channels.SelectionKey; -//import java.nio.channels.SocketChannel; -//import java.time.Duration; -//import java.util.concurrent.CompletableFuture; -// -//public class Socks5ProxyUdpIoClient implements UdpIoClient { -// private final Socks5ProxyIoClientFactory factory; -// private Socks5ProxyIoClientFactory.PoolConn poolConn; -// private SelectionKey udpSelectionKey; -// private final Socks5ProxyConfig config; -// private int max; -// -// public Socks5ProxyUdpIoClient( -// Socks5ProxyIoClientFactory factory, -// Socks5ProxyConfig config) { -// this.factory = factory; -// this.config = config; -// } -// -// public void initOrReuseConn(CompletableFuture f, String keyString, InetSocketAddress local, InetSocketAddress remote) { -// Socks5ProxyIoClientFactory.PoolConn poolConn = factory.getPoolConnFromPool(keyString); -// try { -// if (poolConn == null) { -// SocketChannel tcpConn = SocketChannel.open(); -// tcpConn.configureBlocking(false); -// SelectionKey selectionKey = factory.registerToSelector(tcpConn); -// Socks5Proxy socks5Conn = new Socks5Proxy(selectionKey, config, local, remote, Socks5Proxy.Command.UDP_ASSOCIATE); -// tcpConn.connect(config.getProxyAddress()); -// socks5Conn.handleSOCKS5(f); -// this.poolConn = factory.addConnectionToPool(keyString, socks5Conn); -// } else { -// SelectionKey selectionKey = factory.registerToSelector(poolConn.getSocks5Conn().getTcpSelectionKey().channel()); -// poolConn.getSocks5Conn().setTcpSelectionKey(selectionKey); -// this.poolConn = poolConn; -// f.complete(null); -// } -// } catch (IOException e) { -// f.completeExceptionally(e); -// } -// } -// -// -// @Override -// public CompletableFuture sendAndReceiveUdp( -// InetSocketAddress local, -// InetSocketAddress remote, -// Message query, -// byte[] data, -// int max, -// Duration timeout) { -// this.max = max; -// InetSocketAddress finalLocal; -// if (local == null) { -// finalLocal = new InetSocketAddress(0); -// } else { -// finalLocal = local; -// } -// // keyString is used to identify and reuse SOCKS5 connections -// String keyString = finalLocal.toString() + "-" + remote.toString() + "-UDP"; -// CompletableFuture socksF = new CompletableFuture<>(); -// this.initOrReuseConn(socksF, keyString, finalLocal, remote); -// -// -// return socksF.thenComposeAsync(v -> { -// CompletableFuture dataF = new CompletableFuture<>(); -// try { -// udpSelectionKey = this.poolConn.getSocks5Conn().getUdpChannel().register(factory.getSelector(), SelectionKey.OP_READ); -// udpSelectionKey.selector().wakeup(); -// -// udpSelectionKey.attach(new SendHandler(dataF, data, udpSelectionKey)); -// udpSelectionKey.interestOps(SelectionKey.OP_WRITE); -// udpSelectionKey.selector().wakeup(); -// } catch (Exception e) { -// dataF.completeExceptionally(e); -// } -// return dataF; -// }).whenComplete((result, ex) -> { -// try { -// factory.unregisterFromSelector(poolConn, ex); -// } catch (IOException e) { -// throw new RuntimeException(e); -// } -// }); -// } -// -// private class SendHandler implements Runnable { -// private final CompletableFuture future; -// private final byte[] data; -// private final SelectionKey selectionKey; -// -// public SendHandler(CompletableFuture future, byte[] data, SelectionKey selectionKey) { -// this.future = future; -// this.data = data; -// this.selectionKey = selectionKey; -// } -// -// @Override -// public void run() { -// try { -// DatagramChannel channel = (DatagramChannel) selectionKey.channel(); -// ByteBuffer buffer = poolConn.getSocks5Conn().addSocks5UdpAssociateHeader(data); -// int headerLength = buffer.position()-data.length; -// buffer.flip(); -// while (buffer.hasRemaining()) { -// channel.write(buffer); -// } -// selectionKey.attach(new ReceiveHandler(future, selectionKey, headerLength)); -// selectionKey.interestOps(SelectionKey.OP_READ); -// selectionKey.selector().wakeup(); -// } catch (IOException e) { -// future.completeExceptionally(e); -// } -// } -// } -// -// private class ReceiveHandler implements Runnable { -// private final CompletableFuture future; -// private final SelectionKey selectionKey; -// private final int headerLength; -// -// public ReceiveHandler(CompletableFuture future, SelectionKey selectionKey, int headerLength) { -// this.future = future; -// this.selectionKey = selectionKey; -// this.headerLength = headerLength; -// } -// -// @Override -// public void run() { -// try { -// DatagramChannel channel = (DatagramChannel) selectionKey.channel(); -// ByteBuffer responseData = ByteBuffer.allocate(max); -// int read = channel.read(responseData); -// if (read < 0) { -// throw new EOFException(); -// } -// -// int length = responseData.position() - headerLength; -// byte[] data = new byte[length]; -// responseData.position(headerLength); -// responseData.get(data, 0, length); -// future.complete(data); -// -// selectionKey.cancel(); -// } catch (IOException e) { -// future.completeExceptionally(e); -// } -// } -// } -//} From 46a1b2480e1f9f428b8cc597d4b53e42500508ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 13:35:17 +0100 Subject: [PATCH 18/53] added testcontainer --- pom.xml | 5 +++ .../org/xbill/DNS/io/AbstractSocksTest.java | 14 +++++++ .../org/xbill/DNS/io/SimpleSocksTCPTest.java | 5 +++ src/test/java/org/xbill/DNS/io/docker/.env | 4 ++ .../java/org/xbill/DNS/io/docker/Dockerfile | 37 ++++++++++++++++ .../java/org/xbill/DNS/io/docker/corefile | 9 ++++ .../xbill/DNS/io/docker/docker-compose.yaml | 42 +++++++++++++++++++ .../org/xbill/DNS/io/docker/entrypoint.sh | 13 ++++++ .../java/org/xbill/DNS/io/docker/sockd.conf | 36 ++++++++++++++++ src/test/java/org/xbill/DNS/io/docker/test.db | 3 ++ 10 files changed, 168 insertions(+) create mode 100644 src/test/java/org/xbill/DNS/io/AbstractSocksTest.java create mode 100644 src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java create mode 100644 src/test/java/org/xbill/DNS/io/docker/.env create mode 100644 src/test/java/org/xbill/DNS/io/docker/Dockerfile create mode 100644 src/test/java/org/xbill/DNS/io/docker/corefile create mode 100644 src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml create mode 100755 src/test/java/org/xbill/DNS/io/docker/entrypoint.sh create mode 100644 src/test/java/org/xbill/DNS/io/docker/sockd.conf create mode 100644 src/test/java/org/xbill/DNS/io/docker/test.db diff --git a/pom.xml b/pom.xml index e033b92f..4d43de0d 100644 --- a/pom.xml +++ b/pom.xml @@ -685,6 +685,11 @@ 0.16 test + + org.testcontainers + docker-compose + 0.9.9 + diff --git a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java new file mode 100644 index 00000000..072ec269 --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java @@ -0,0 +1,14 @@ +package org.xbill.DNS.io; + +import org.testcontainers.containers.GenericContainer; + +public class AbstractSocksTest { + + public static final GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) + .withExposedPorts(6379); + + static { + redis.start(); + } + +} diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java new file mode 100644 index 00000000..d69efd01 --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java @@ -0,0 +1,5 @@ +package org.xbill.DNS.io; + +public class SimpleSocksTCPTest { + +} diff --git a/src/test/java/org/xbill/DNS/io/docker/.env b/src/test/java/org/xbill/DNS/io/docker/.env new file mode 100644 index 00000000..3d970a2c --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/docker/.env @@ -0,0 +1,4 @@ +COMPOSE_PROJECT_NAME=dnsjava-test +SOCKD_PORT=1080 +SOCKD_USER_NAME=me +SOCKD_USER_PASSWORD=42 diff --git a/src/test/java/org/xbill/DNS/io/docker/Dockerfile b/src/test/java/org/xbill/DNS/io/docker/Dockerfile new file mode 100644 index 00000000..9158886c --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/docker/Dockerfile @@ -0,0 +1,37 @@ +# inspired by https://github.com/adegtyarev/docker-dante +FROM alpine:3.20 + +ARG SOCKD_USER_NAME +ARG SOCKD_USER_PASSWORD + +ENV SOCKD_USER_NAME ${SOCKD_USER_NAME} +ENV SOCKD_USER_PASSWORD ${SOCKD_USER_PASSWORD} + +ENV DANTE_VER 1.4.2 +ENV DANTE_URL https://www.inet.no/dante/files/dante-$DANTE_VER.tar.gz +ENV DANTE_SHA 4c97cff23e5c9b00ca1ec8a95ab22972813921d7fbf60fc453e3e06382fc38a7 + +RUN apk add --no-cache --virtual .build-deps \ + build-base \ + curl \ + linux-pam-dev && \ + install -v -d /src && \ + curl -sSL $DANTE_URL -o /src/dante.tar.gz && \ + echo "$DANTE_SHA */src/dante.tar.gz" | sha256sum -c && \ + tar -C /src -vxzf /src/dante.tar.gz && \ + cd /src/dante-$DANTE_VER && \ + # https://lists.alpinelinux.org/alpine-devel/3932.html + ac_cv_func_sched_setscheduler=no ./configure --build=aarch64-unknown-linux-gnu && \ + make -j install && \ + cd / && rm -r /src && \ + apk del .build-deps && \ + apk add --no-cache \ + linux-pam + +COPY sockd.conf /etc/ +COPY entrypoint.sh / + +EXPOSE 1080 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["sockd"] diff --git a/src/test/java/org/xbill/DNS/io/docker/corefile b/src/test/java/org/xbill/DNS/io/docker/corefile new file mode 100644 index 00000000..deeb53c1 --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/docker/corefile @@ -0,0 +1,9 @@ +# RFC 2606: Reserved Top Level DNS Names (https://www.rfc-editor.org/rfc/rfc2606.html) +test.:53 { + debug + log + errors + health + file /etc/coredns/test.db +} + diff --git a/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml b/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml new file mode 100644 index 00000000..b7600a94 --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml @@ -0,0 +1,42 @@ + +services: + coredns: + image: coredns/coredns:1.12.0 + networks: + default: + ipv4_address: 10.5.0.2 + ipv6_address: 2001:db8::2 + volumes: + - "./coredns:/etc/coredns" + command: "-conf /etc/coredns/corefile" + ports: + - "53/udp" + + dante-socks5: + build: + context: . + dockerfile: Dockerfile + privileged: true + env_file: + - .env + ports: + - "${SOCKD_PORT}:1080" + - "10000:10000/udp" + - "10001:10001/udp" + networks: + default: + ipv4_address: 10.5.0.3 + ipv6_address: 2001:db8::3 + volumes: + - ./sockd.conf:/etc/sockd.conf + +networks: + default: + driver: bridge + enable_ipv6: true + ipam: + config: + - subnet: 10.5.0.0/16 + gateway: 10.5.0.1 + - subnet: 2001:db8::/64 + gateway: 2001:db8::1 diff --git a/src/test/java/org/xbill/DNS/io/docker/entrypoint.sh b/src/test/java/org/xbill/DNS/io/docker/entrypoint.sh new file mode 100755 index 00000000..440154ef --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/docker/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh +if [ -n "${SOCKD_USER_NAME}" ]; then + echo "${SOCKD_USER_NAME}" + if [ -z "${SOCKD_USER_PASSWORD}" ]; then + echo "Set SOCKD_USER_PASSWORD in .env" + exit 1 + fi + adduser -D "${SOCKD_USER_NAME}" + echo "${SOCKD_USER_NAME}:${SOCKD_USER_PASSWORD}" | chpasswd + echo "user ${SOCKD_USER_NAME} successfully set" +fi +exec "$@" + diff --git a/src/test/java/org/xbill/DNS/io/docker/sockd.conf b/src/test/java/org/xbill/DNS/io/docker/sockd.conf new file mode 100644 index 00000000..3ad5ec86 --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/docker/sockd.conf @@ -0,0 +1,36 @@ +# logging +errorlog: ./sockd.errlog +logoutput: ./sockd.log + +# server address specification +internal: 0.0.0.0 port = 1088 +external: eth0 +#external.rotation: same-same + +# auth +#user.privileged: root +user.notprivileged: nobody +socksmethod: none # "none" or "username" user/pwd authentication + +# Allow everyone to connect to this server. +client pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + log: connect disconnect error # disconnect +} + +# Allow all operations for connected clients on this server. +socks pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + command: bind connect udpassociate + log: connect disconnect iooperation error + socksmethod: none # "none" or "username" user/pwd authentication + + udp.portrange: 10000-10001 +} + +# Allow all inbound packets. +socks pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + command: bindreply udpreply + log: error connect disconnect iooperation +} diff --git a/src/test/java/org/xbill/DNS/io/docker/test.db b/src/test/java/org/xbill/DNS/io/docker/test.db new file mode 100644 index 00000000..11ed72ec --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/docker/test.db @@ -0,0 +1,3 @@ +test. IN SOA test. admin.test. 1675303881 7200 3600 1209600 3600 + +simple.test. IN A 1.2.3.4 From 66a1e1db3d6b73d07255e67769ff20613aae030b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 14:04:17 +0100 Subject: [PATCH 19/53] adjusted corefile path --- src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml b/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml index b7600a94..5a9cc49f 100644 --- a/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml +++ b/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml @@ -7,7 +7,7 @@ services: ipv4_address: 10.5.0.2 ipv6_address: 2001:db8::2 volumes: - - "./coredns:/etc/coredns" + - "./corefile:/etc/coredns/corefile" command: "-conf /etc/coredns/corefile" ports: - "53/udp" From ba1ea1cf49de9ac871a4109ad8488faa834fbc6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 14:15:46 +0100 Subject: [PATCH 20/53] adjusted corefile path --- src/test/java/org/xbill/DNS/io/docker/{ => coredns}/corefile | 0 src/test/java/org/xbill/DNS/io/docker/{ => coredns}/test.db | 0 src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename src/test/java/org/xbill/DNS/io/docker/{ => coredns}/corefile (100%) rename src/test/java/org/xbill/DNS/io/docker/{ => coredns}/test.db (100%) diff --git a/src/test/java/org/xbill/DNS/io/docker/corefile b/src/test/java/org/xbill/DNS/io/docker/coredns/corefile similarity index 100% rename from src/test/java/org/xbill/DNS/io/docker/corefile rename to src/test/java/org/xbill/DNS/io/docker/coredns/corefile diff --git a/src/test/java/org/xbill/DNS/io/docker/test.db b/src/test/java/org/xbill/DNS/io/docker/coredns/test.db similarity index 100% rename from src/test/java/org/xbill/DNS/io/docker/test.db rename to src/test/java/org/xbill/DNS/io/docker/coredns/test.db diff --git a/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml b/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml index 5a9cc49f..b7600a94 100644 --- a/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml +++ b/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml @@ -7,7 +7,7 @@ services: ipv4_address: 10.5.0.2 ipv6_address: 2001:db8::2 volumes: - - "./corefile:/etc/coredns/corefile" + - "./coredns:/etc/coredns" command: "-conf /etc/coredns/corefile" ports: - "53/udp" From baf40f279db3a566aff271f6712df47821fde439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 15:56:18 +0100 Subject: [PATCH 21/53] adjusted docker compose setup --- src/test/java/org/xbill/DNS/io/docker/sockd.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/xbill/DNS/io/docker/sockd.conf b/src/test/java/org/xbill/DNS/io/docker/sockd.conf index 3ad5ec86..6d07631d 100644 --- a/src/test/java/org/xbill/DNS/io/docker/sockd.conf +++ b/src/test/java/org/xbill/DNS/io/docker/sockd.conf @@ -3,7 +3,7 @@ errorlog: ./sockd.errlog logoutput: ./sockd.log # server address specification -internal: 0.0.0.0 port = 1088 +internal: 0.0.0.0 port = 1080 external: eth0 #external.rotation: same-same From ba11e90153f47878e38ee9236b96e4cdb4fbc4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 15:58:49 +0100 Subject: [PATCH 22/53] adjusted docker compose setup --- src/test/java/org/xbill/DNS/io/docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/xbill/DNS/io/docker/Dockerfile b/src/test/java/org/xbill/DNS/io/docker/Dockerfile index 9158886c..4f052123 100644 --- a/src/test/java/org/xbill/DNS/io/docker/Dockerfile +++ b/src/test/java/org/xbill/DNS/io/docker/Dockerfile @@ -14,6 +14,7 @@ ENV DANTE_SHA 4c97cff23e5c9b00ca1ec8a95ab22972813921d7fbf60fc453e3e06382fc38 RUN apk add --no-cache --virtual .build-deps \ build-base \ curl \ + bind-tools \ linux-pam-dev && \ install -v -d /src && \ curl -sSL $DANTE_URL -o /src/dante.tar.gz && \ From 8b71b0957eca8790d110e62df9ce44b645938f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 20:30:12 +0100 Subject: [PATCH 23/53] implemented simple test for DNS over Socks --- pom.xml | 6 +- .../org/xbill/DNS/io/AbstractSocksTest.java | 24 +++++-- .../org/xbill/DNS/io/SimpleSocksTCPTest.java | 5 -- .../org/xbill/DNS/io/SimpleSocksTest.java | 68 +++++++++++++++++++ .../DNS/io/docker => resources/compose}/.env | 0 .../docker => resources/compose}/Dockerfile | 1 + .../compose/compose.yml} | 4 +- .../compose}/coredns/corefile | 0 .../compose}/coredns/test.db | 0 .../compose}/entrypoint.sh | 0 .../docker => resources/compose}/sockd.conf | 9 ++- 11 files changed, 97 insertions(+), 20 deletions(-) delete mode 100644 src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java create mode 100644 src/test/java/org/xbill/DNS/io/SimpleSocksTest.java rename src/test/{java/org/xbill/DNS/io/docker => resources/compose}/.env (100%) rename src/test/{java/org/xbill/DNS/io/docker => resources/compose}/Dockerfile (98%) rename src/test/{java/org/xbill/DNS/io/docker/docker-compose.yaml => resources/compose/compose.yml} (87%) rename src/test/{java/org/xbill/DNS/io/docker => resources/compose}/coredns/corefile (100%) rename src/test/{java/org/xbill/DNS/io/docker => resources/compose}/coredns/test.db (100%) rename src/test/{java/org/xbill/DNS/io/docker => resources/compose}/entrypoint.sh (100%) rename src/test/{java/org/xbill/DNS/io/docker => resources/compose}/sockd.conf (75%) diff --git a/pom.xml b/pom.xml index 4d43de0d..434faebc 100644 --- a/pom.xml +++ b/pom.xml @@ -687,9 +687,11 @@ org.testcontainers - docker-compose - 0.9.9 + testcontainers + 1.20.4 + test + diff --git a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java index 072ec269..8cc844a6 100644 --- a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java @@ -1,14 +1,24 @@ package org.xbill.DNS.io; -import org.testcontainers.containers.GenericContainer; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; -public class AbstractSocksTest { +import java.io.File; +import java.time.Duration; - public static final GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) - .withExposedPorts(6379); - static { - redis.start(); - } +public class AbstractSocksTest { + static final DockerComposeContainer environment = new DockerComposeContainer( + new File("src/test/resources/compose/compose.yml") + ).withBuild(true).withStartupTimeout(Duration.ofSeconds(3000)) + .waitingFor("dante-socks5", Wait.forHealthcheck()); + @Test + public void setup() { + environment.start(); + System.out.println("Container started"); + System.out.println("Container ready"); + environment.stop(); + } } diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java deleted file mode 100644 index d69efd01..00000000 --- a/src/test/java/org/xbill/DNS/io/SimpleSocksTCPTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.xbill.DNS.io; - -public class SimpleSocksTCPTest { - -} diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java new file mode 100644 index 00000000..5fa7040b --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -0,0 +1,68 @@ +package org.xbill.DNS.io; + +import org.junit.jupiter.api.Test; +import org.xbill.DNS.*; +import org.xbill.DNS.Record; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; + + +public class SimpleSocksTest extends AbstractSocksTest { + + @Test + public void testUDP() throws IOException { + environment.start(); + + SimpleResolver res = new SimpleResolver(); + InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); + NioSocks5ProxyConfig config = new NioSocks5ProxyConfig(proxyAddress); + res.setIoClientFactory(new NioSocks5ProxyFactory(config)); + + Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); + Message query = Message.newQuery(rec); + Message response = res.send(query); + System.out.println(response); + + environment.stop(); + } + + @Test + public void testTCP() throws IOException { + environment.start(); + + SimpleResolver res = new SimpleResolver("10.5.0.2"); + InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); + NioSocks5ProxyConfig config = new NioSocks5ProxyConfig(proxyAddress); + res.setIoClientFactory(new NioSocks5ProxyFactory(config)); + res.setTCP(true); + + Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); + Message query = Message.newQuery(rec); + Message response = res.send(query); + System.out.println(response); + + environment.stop(); + } + + @Test + public void testAuth() throws IOException { + environment.start(); + + SimpleResolver res = new SimpleResolver(); + InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); + String socks5User = "me"; + String socks5Password = "42"; + NioSocks5ProxyConfig config = new NioSocks5ProxyConfig(proxyAddress, socks5User, socks5Password); + res.setIoClientFactory(new NioSocks5ProxyFactory(config)); + + Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); + Message query = Message.newQuery(rec); + Message response = res.send(query); + System.out.println(response); + + environment.stop(); + } + +} diff --git a/src/test/java/org/xbill/DNS/io/docker/.env b/src/test/resources/compose/.env similarity index 100% rename from src/test/java/org/xbill/DNS/io/docker/.env rename to src/test/resources/compose/.env diff --git a/src/test/java/org/xbill/DNS/io/docker/Dockerfile b/src/test/resources/compose/Dockerfile similarity index 98% rename from src/test/java/org/xbill/DNS/io/docker/Dockerfile rename to src/test/resources/compose/Dockerfile index 4f052123..f3bff7bb 100644 --- a/src/test/java/org/xbill/DNS/io/docker/Dockerfile +++ b/src/test/resources/compose/Dockerfile @@ -27,6 +27,7 @@ RUN apk add --no-cache --virtual .build-deps \ cd / && rm -r /src && \ apk del .build-deps && \ apk add --no-cache \ + curl \ linux-pam COPY sockd.conf /etc/ diff --git a/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml b/src/test/resources/compose/compose.yml similarity index 87% rename from src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml rename to src/test/resources/compose/compose.yml index b7600a94..9e8321c3 100644 --- a/src/test/java/org/xbill/DNS/io/docker/docker-compose.yaml +++ b/src/test/resources/compose/compose.yml @@ -20,13 +20,15 @@ services: env_file: - .env ports: - - "${SOCKD_PORT}:1080" + - "1080:1080" - "10000:10000/udp" - "10001:10001/udp" networks: default: ipv4_address: 10.5.0.3 ipv6_address: 2001:db8::3 + healthcheck: + test: "curl -x socks5h://localhost:1080 http://coredns:8080/health" volumes: - ./sockd.conf:/etc/sockd.conf diff --git a/src/test/java/org/xbill/DNS/io/docker/coredns/corefile b/src/test/resources/compose/coredns/corefile similarity index 100% rename from src/test/java/org/xbill/DNS/io/docker/coredns/corefile rename to src/test/resources/compose/coredns/corefile diff --git a/src/test/java/org/xbill/DNS/io/docker/coredns/test.db b/src/test/resources/compose/coredns/test.db similarity index 100% rename from src/test/java/org/xbill/DNS/io/docker/coredns/test.db rename to src/test/resources/compose/coredns/test.db diff --git a/src/test/java/org/xbill/DNS/io/docker/entrypoint.sh b/src/test/resources/compose/entrypoint.sh similarity index 100% rename from src/test/java/org/xbill/DNS/io/docker/entrypoint.sh rename to src/test/resources/compose/entrypoint.sh diff --git a/src/test/java/org/xbill/DNS/io/docker/sockd.conf b/src/test/resources/compose/sockd.conf similarity index 75% rename from src/test/java/org/xbill/DNS/io/docker/sockd.conf rename to src/test/resources/compose/sockd.conf index 6d07631d..250aa7b8 100644 --- a/src/test/java/org/xbill/DNS/io/docker/sockd.conf +++ b/src/test/resources/compose/sockd.conf @@ -1,16 +1,15 @@ # logging -errorlog: ./sockd.errlog -logoutput: ./sockd.log +errorlog: /var/log/sockd.errlog +logoutput: /var/log/sockd.log # server address specification internal: 0.0.0.0 port = 1080 external: eth0 -#external.rotation: same-same # auth #user.privileged: root user.notprivileged: nobody -socksmethod: none # "none" or "username" user/pwd authentication +socksmethod: none username # "none" or "username" user/pwd authentication # Allow everyone to connect to this server. client pass { @@ -23,7 +22,7 @@ socks pass { from: 0.0.0.0/0 to: 0.0.0.0/0 command: bind connect udpassociate log: connect disconnect iooperation error - socksmethod: none # "none" or "username" user/pwd authentication + socksmethod: none username # "none" or "username" user/pwd authentication udp.portrange: 10000-10001 } From c0ffa8787c9cd8c70d0533f3535920f13d62c14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 20:52:46 +0100 Subject: [PATCH 24/53] implemented simple test for DNS over Socks --- .../org/xbill/DNS/io/SimpleSocksTest.java | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java index 5fa7040b..dc64539e 100644 --- a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -1,5 +1,7 @@ package org.xbill.DNS.io; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.xbill.DNS.*; import org.xbill.DNS.Record; @@ -8,61 +10,53 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class SimpleSocksTest extends AbstractSocksTest { - @Test - public void testUDP() throws IOException { + @BeforeAll + public static void setUp() throws IOException { environment.start(); + } - SimpleResolver res = new SimpleResolver(); + @AfterAll + public static void tearDown() throws IOException { + environment.stop(); + } + + private SimpleResolver createResolver(String address, boolean useTCP, String user, String password) throws IOException { + SimpleResolver res = address == null ? new SimpleResolver() : new SimpleResolver(address); InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); - NioSocks5ProxyConfig config = new NioSocks5ProxyConfig(proxyAddress); + NioSocks5ProxyConfig config = user == null ? new NioSocks5ProxyConfig(proxyAddress) : new NioSocks5ProxyConfig(proxyAddress, user, password); res.setIoClientFactory(new NioSocks5ProxyFactory(config)); + res.setTCP(useTCP); + return res; + } + @Test + public void testUDP() throws IOException { + SimpleResolver res = createResolver(null, false, null, null); Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); Message query = Message.newQuery(rec); Message response = res.send(query); - System.out.println(response); - - environment.stop(); + assertNotNull(response); } @Test public void testTCP() throws IOException { - environment.start(); - - SimpleResolver res = new SimpleResolver("10.5.0.2"); - InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); - NioSocks5ProxyConfig config = new NioSocks5ProxyConfig(proxyAddress); - res.setIoClientFactory(new NioSocks5ProxyFactory(config)); - res.setTCP(true); - + SimpleResolver res = createResolver("10.5.0.2", true, null, null); Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); Message query = Message.newQuery(rec); Message response = res.send(query); - System.out.println(response); - - environment.stop(); + assertNotNull(response); } @Test public void testAuth() throws IOException { - environment.start(); - - SimpleResolver res = new SimpleResolver(); - InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); - String socks5User = "me"; - String socks5Password = "42"; - NioSocks5ProxyConfig config = new NioSocks5ProxyConfig(proxyAddress, socks5User, socks5Password); - res.setIoClientFactory(new NioSocks5ProxyFactory(config)); - + SimpleResolver res = createResolver(null, false, "me", "42"); Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); Message query = Message.newQuery(rec); Message response = res.send(query); - System.out.println(response); - - environment.stop(); + assertNotNull(response); } - } From f36df784fd2534ed8f1d205ab4e3270033e89dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 21:06:34 +0100 Subject: [PATCH 25/53] implemented simple test for DNS over Socks --- src/test/java/org/xbill/DNS/io/AbstractSocksTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java index 8cc844a6..036a6e9f 100644 --- a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java @@ -1,7 +1,7 @@ package org.xbill.DNS.io; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import java.io.File; @@ -9,7 +9,7 @@ public class AbstractSocksTest { - static final DockerComposeContainer environment = new DockerComposeContainer( + static final ComposeContainer environment = new ComposeContainer( new File("src/test/resources/compose/compose.yml") ).withBuild(true).withStartupTimeout(Duration.ofSeconds(3000)) .waitingFor("dante-socks5", Wait.forHealthcheck()); From 240d206d7041ff5e3934ce16039765f71fcc2066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 21:19:08 +0100 Subject: [PATCH 26/53] implemented simple test for DNS over Socks --- src/test/java/org/xbill/DNS/io/AbstractSocksTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java index 036a6e9f..29165690 100644 --- a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java @@ -11,8 +11,10 @@ public class AbstractSocksTest { static final ComposeContainer environment = new ComposeContainer( new File("src/test/resources/compose/compose.yml") - ).withBuild(true).withStartupTimeout(Duration.ofSeconds(3000)) - .waitingFor("dante-socks5", Wait.forHealthcheck()); + ) + .withBuild(true) + .waitingFor("dante-socks5", Wait.forHealthcheck()) + .withLocalCompose(true); @Test public void setup() { From c807b025215c9ed8f2e6c7e8fb62ef99694a9c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 21:23:33 +0100 Subject: [PATCH 27/53] implemented simple test for DNS over Socks --- src/test/resources/compose/compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/resources/compose/compose.yml b/src/test/resources/compose/compose.yml index 9e8321c3..d741dcf0 100644 --- a/src/test/resources/compose/compose.yml +++ b/src/test/resources/compose/compose.yml @@ -16,7 +16,6 @@ services: build: context: . dockerfile: Dockerfile - privileged: true env_file: - .env ports: From d0f3460c958967b8218ef992c118ebe6176ad04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 21:24:18 +0100 Subject: [PATCH 28/53] implemented simple test for DNS over Socks --- src/test/java/org/xbill/DNS/io/AbstractSocksTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java index 29165690..656781bb 100644 --- a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java @@ -13,8 +13,7 @@ public class AbstractSocksTest { new File("src/test/resources/compose/compose.yml") ) .withBuild(true) - .waitingFor("dante-socks5", Wait.forHealthcheck()) - .withLocalCompose(true); + .waitingFor("dante-socks5", Wait.forHealthcheck()); @Test public void setup() { From 1f88de9ddb306db6fcb48606e17c73034cb70c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 22:30:52 +0100 Subject: [PATCH 29/53] adjusting changes to upstream --- .../java/org/xbill/DNS/NioSocksHandler.java | 7 +++-- .../java/org/xbill/DNS/NioTcpHandler.java | 29 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index cda4f716..3731d3f6 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -85,7 +85,8 @@ public CompletableFuture doAuthHandshake( methodSelectionRequest.toBytes(), endTime, channel.getChannel(), - methodSelectionF); + methodSelectionF, + true); channel.queueTransaction(methodSelectionTransaction); methodSelectionF.thenComposeAsync( @@ -128,7 +129,7 @@ private CompletableFuture handleUserPassAuth( UserPassAuthRequest userPassAuthRequest = new UserPassAuthRequest(socks5User, socks5Password); NioTcpHandler.Transaction userPwdAuthTransaction = new NioTcpHandler.Transaction( - query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassAuthF); + query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassAuthF, true); channel.queueTransaction(userPwdAuthTransaction); userPassAuthF.thenComposeAsync( @@ -163,7 +164,7 @@ public CompletableFuture doSocks5Request( CmdRequest cmdRequest = new CmdRequest(socks5Cmd, address); NioTcpHandler.Transaction commandTransaction = new NioTcpHandler.Transaction( - query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF); + query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF, true); channel.queueTransaction(commandTransaction); commandF.thenComposeAsync( diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 62d4b0be..76962ce6 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -83,6 +83,7 @@ public static class Transaction { private final long endTime; private final SocketChannel channel; private final CompletableFuture f; + private final boolean isSocks5; private ByteBuffer queryDataBuffer; long bytesWrittenTotal = 0; @@ -92,13 +93,17 @@ boolean send() throws IOException { return true; } if (queryDataBuffer == null) { - // combine length+message to avoid multiple TCP packets - // https://datatracker.ietf.org/doc/html/rfc7766#section-8 - queryDataBuffer = ByteBuffer.allocate(queryData.length + 2); - queryDataBuffer.put((byte) (queryData.length >>> 8)); - queryDataBuffer.put((byte) (queryData.length & 0xFF)); - queryDataBuffer.put(queryData); - queryDataBuffer.flip(); + if (isSocks5) { + queryDataBuffer = ByteBuffer.wrap(queryData); + } else { + // combine length+message to avoid multiple TCP packets + // https://datatracker.ietf.org/doc/html/rfc7766#section-8 + queryDataBuffer = ByteBuffer.allocate(queryData.length + 2); + queryDataBuffer.put((byte) (queryData.length >>> 8)); + queryDataBuffer.put((byte) (queryData.length & 0xFF)); + queryDataBuffer.put(queryData); + queryDataBuffer.flip(); + } } verboseLog( @@ -140,7 +145,7 @@ public class ChannelState implements KeyProcessor { ByteBuffer responseLengthData = ByteBuffer.allocate(2); ByteBuffer responseData = ByteBuffer.allocate(Message.MAXLENGTH); int readState = 0; - boolean isSocks5 = false; + boolean isSocks5; CompletableFuture socks5HandshakeF; @Override @@ -329,13 +334,7 @@ public void dnsTransaction( ChannelState channel, Message query, byte[] data, long endTime, CompletableFuture f) { // Transaction for the main data channel.setSocks5(false); - // combine length+message to avoid multiple TCP packets - // https://datatracker.ietf.org/doc/html/rfc7766#section-8 - ByteBuffer buffer = ByteBuffer.allocate(2 + data.length); - buffer.put((byte) (data.length >>> 8)); - buffer.put((byte) (data.length & 0xFF)); - buffer.put(data); - Transaction t = new Transaction(query, buffer.array(), endTime, channel.channel, f); + Transaction t = new Transaction(query, data, endTime, channel.channel, f, false); channel.queueTransaction(t); } From 02ef9982e5ff9bd79fd0a490f46d091caae51c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 22:42:16 +0100 Subject: [PATCH 30/53] adjusting changes to upstream --- src/main/java/org/xbill/DNS/NioTcpHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 76962ce6..b3b82dd9 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -72,7 +72,11 @@ private void checkTransactionTimeouts() { private void closeTcp() { registrationQueue.clear(); EOFException closing = new EOFException("Client is closing"); - channelMap.forEach((key, state) -> state.handleTransactionException(closing)); + channelMap.forEach( + (key, state) -> { + state.handleTransactionException(closing); + state.handleChannelException(closing); + }); channelMap.clear(); } From 3970b974a3777193af1a234336e12667ff5aedff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 23:03:36 +0100 Subject: [PATCH 31/53] further clean up --- src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java | 5 ----- src/main/java/org/xbill/DNS/NioSocksHandler.java | 2 +- src/main/java/org/xbill/DNS/NioSocksUdpClient.java | 3 +-- src/main/java/org/xbill/DNS/NioTcpHandler.java | 4 +--- src/main/java/org/xbill/DNS/NioUdpHandler.java | 6 ------ src/test/resources/compose/sockd.conf | 1 - 6 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java b/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java index 035ae080..d6707633 100644 --- a/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java +++ b/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java @@ -11,15 +11,10 @@ @Getter @Slf4j public class NioSocks5ProxyFactory implements IoClientFactory { - - // SOCKS5 proxy configuration private final NioSocks5ProxyConfig config; - - // io clients private final TcpIoClient tcpIoClient; private final UdpIoClient udpIoClient; - // constructor public NioSocks5ProxyFactory(NioSocks5ProxyConfig socks5Proxy) { config = Objects.requireNonNull(socks5Proxy, "proxy config must not be null"); tcpIoClient = new NioSocksTcpClient(config); diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 3731d3f6..396556db 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -249,7 +249,7 @@ public synchronized CompletableFuture doSocks5Handshake( return socks5HandshakeF; } - public byte[] addUdpHeader(byte[] data, InetSocketAddress to) { + public byte[] addUdpHeader(byte[] data) { ByteBuffer buffer; byte addressType; byte[] addressBytes; diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index f6c4a542..fb6ea69c 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -51,11 +51,10 @@ public CompletableFuture sendAndReceiveUdp( .socks5HandshakeF .thenApplyAsync( cmdBytes -> { + byte[] wrappedData = proxy.addUdpHeader(data); NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); InetSocketAddress newRemote = new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); - byte[] wrappedData = proxy.addUdpHeader(data, newRemote); - udpHandler .sendAndReceiveUdp( local, newRemote, channel.getUdpChannel(), query, wrappedData, max, timeout) diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index b3b82dd9..06bd953d 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -336,7 +336,6 @@ static class ChannelKey { public void dnsTransaction( ChannelState channel, Message query, byte[] data, long endTime, CompletableFuture f) { - // Transaction for the main data channel.setSocks5(false); Transaction t = new Transaction(query, data, endTime, channel.channel, f, false); channel.queueTransaction(t); @@ -412,7 +411,7 @@ public CompletableFuture sendAndReceiveTcp( channel, NioSocksHandler.SOCKS5_CMD_CONNECT, query, endTime); } } - // Chain the SOCKS5 transactions with the TCP data transaction + // Chain the SOCKS5 transactions with the DNS data transaction channel .socks5HandshakeF .thenRunAsync( @@ -425,7 +424,6 @@ public CompletableFuture sendAndReceiveTcp( return null; }); } else { - // transaction for DNS data dnsTransaction(channel, query, data, endTime, f); } } diff --git a/src/main/java/org/xbill/DNS/NioUdpHandler.java b/src/main/java/org/xbill/DNS/NioUdpHandler.java index 332eb990..01f2f4fc 100644 --- a/src/main/java/org/xbill/DNS/NioUdpHandler.java +++ b/src/main/java/org/xbill/DNS/NioUdpHandler.java @@ -28,7 +28,6 @@ final class NioUdpHandler extends NioClient { private final SecureRandom prng; private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); private static final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); - private final NioSocksUdpAssociateChannelPool udpPool; NioUdpHandler() { @@ -84,7 +83,6 @@ private void checkTransactionTimeouts() { } } - // check for idle channels and remove them udpPool.removeIdleChannels(); } @@ -165,10 +163,6 @@ public void processReadyKey(SelectionKey key) { // do not close the channel in case of SOCKS5 UDP associate. // the channel port needs to be claimed for further queries to the same remote host. // you can not use the same UDP associate port with another local port after the first query. - // you can also close this channel and open a new one with the same local port for further - // queries, - // but I would like to avoid, that the local port will be taken by another process between - // queries. if (!isProxyChannel) { silentDisconnectAndCloseChannel(); } diff --git a/src/test/resources/compose/sockd.conf b/src/test/resources/compose/sockd.conf index 250aa7b8..178c0e3f 100644 --- a/src/test/resources/compose/sockd.conf +++ b/src/test/resources/compose/sockd.conf @@ -7,7 +7,6 @@ internal: 0.0.0.0 port = 1080 external: eth0 # auth -#user.privileged: root user.notprivileged: nobody socksmethod: none username # "none" or "username" user/pwd authentication From 00bdea2dd8c41f76fa0a5941c236c0ee8adf5dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sat, 25 Jan 2025 23:58:48 +0100 Subject: [PATCH 32/53] further clean up --- .../java/org/xbill/DNS/NioSocksTcpClient.java | 2 -- .../org/xbill/DNS/io/AbstractSocksTest.java | 25 ------------------- .../org/xbill/DNS/io/SimpleSocksTest.java | 16 +++++++++--- src/test/resources/compose/compose.yml | 2 +- 4 files changed, 13 insertions(+), 32 deletions(-) delete mode 100644 src/test/java/org/xbill/DNS/io/AbstractSocksTest.java diff --git a/src/main/java/org/xbill/DNS/NioSocksTcpClient.java b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java index cfbdb5f4..bde2b62f 100644 --- a/src/main/java/org/xbill/DNS/NioSocksTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksTcpClient.java @@ -10,9 +10,7 @@ @Slf4j final class NioSocksTcpClient extends NioTcpHandler implements TcpIoClient { - // TCP handler private final NioTcpHandler tcpHandler; - // SOCKS5 proxy configuration private final NioSocks5ProxyConfig socksConfig; NioSocksTcpClient(NioSocks5ProxyConfig config) { diff --git a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java b/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java deleted file mode 100644 index 656781bb..00000000 --- a/src/test/java/org/xbill/DNS/io/AbstractSocksTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.xbill.DNS.io; - -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.ComposeContainer; -import org.testcontainers.containers.wait.strategy.Wait; - -import java.io.File; -import java.time.Duration; - - -public class AbstractSocksTest { - static final ComposeContainer environment = new ComposeContainer( - new File("src/test/resources/compose/compose.yml") - ) - .withBuild(true) - .waitingFor("dante-socks5", Wait.forHealthcheck()); - - @Test - public void setup() { - environment.start(); - System.out.println("Container started"); - System.out.println("Container ready"); - environment.stop(); - } -} diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java index dc64539e..6aab08d8 100644 --- a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -3,16 +3,24 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.xbill.DNS.*; import org.xbill.DNS.Record; +import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import static org.junit.jupiter.api.Assertions.assertNotNull; -public class SimpleSocksTest extends AbstractSocksTest { +public class SimpleSocksTest { + static final ComposeContainer environment = new ComposeContainer( + new File("src/test/resources/compose/compose.yml") + ) + .withBuild(true) + .waitingFor("dante-socks5", Wait.forHealthcheck()); @BeforeAll public static void setUp() throws IOException { @@ -20,7 +28,7 @@ public static void setUp() throws IOException { } @AfterAll - public static void tearDown() throws IOException { + public static void tearDown() { environment.stop(); } @@ -35,7 +43,7 @@ private SimpleResolver createResolver(String address, boolean useTCP, String use @Test public void testUDP() throws IOException { - SimpleResolver res = createResolver(null, false, null, null); + SimpleResolver res = createResolver("10.5.0.2", false, null, null); Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); Message query = Message.newQuery(rec); Message response = res.send(query); @@ -53,7 +61,7 @@ public void testTCP() throws IOException { @Test public void testAuth() throws IOException { - SimpleResolver res = createResolver(null, false, "me", "42"); + SimpleResolver res = createResolver("10.5.0.2", false, "me", "42"); Record rec = Record.newRecord(Name.fromString("simple.test", Name.root), Type.A, DClass.IN); Message query = Message.newQuery(rec); Message response = res.send(query); diff --git a/src/test/resources/compose/compose.yml b/src/test/resources/compose/compose.yml index d741dcf0..9c0636d4 100644 --- a/src/test/resources/compose/compose.yml +++ b/src/test/resources/compose/compose.yml @@ -27,7 +27,7 @@ services: ipv4_address: 10.5.0.3 ipv6_address: 2001:db8::3 healthcheck: - test: "curl -x socks5h://localhost:1080 http://coredns:8080/health" + test: "curl -x socks5://127.0.0.1:1080 http://10.5.0.2:8080/health" volumes: - ./sockd.conf:/etc/sockd.conf From 688fe6dee3b9f9f48814285b55b1a06178bc04f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 00:06:33 +0100 Subject: [PATCH 33/53] further clean up --- src/test/java/org/xbill/DNS/io/SimpleSocksTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java index 6aab08d8..a647f662 100644 --- a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -23,8 +23,10 @@ public class SimpleSocksTest { .waitingFor("dante-socks5", Wait.forHealthcheck()); @BeforeAll - public static void setUp() throws IOException { + public static void setUp() throws IOException, InterruptedException { environment.start(); + // sleep 1 second to make sure the container is ready + Thread.sleep(1000); } @AfterAll From e94d15cb8c058cc9b7bc8d96d04426d1128f4b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 00:09:02 +0100 Subject: [PATCH 34/53] further clean up --- src/test/java/org/xbill/DNS/io/SimpleSocksTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java index a647f662..7281a1ad 100644 --- a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -25,8 +25,8 @@ public class SimpleSocksTest { @BeforeAll public static void setUp() throws IOException, InterruptedException { environment.start(); - // sleep 1 second to make sure the container is ready - Thread.sleep(1000); + // wait to make sure the container is ready + Thread.sleep(100); } @AfterAll From b621b3229f6c292eef22eda383b5bf61459b7e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 17:33:43 +0100 Subject: [PATCH 35/53] improved comment for DST.ADDR and DST.PORT in the SOCKS handshake --- src/main/java/org/xbill/DNS/NioSocksHandler.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 396556db..82730418 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -153,12 +153,12 @@ public CompletableFuture doSocks5Request( NioTcpHandler.ChannelState channel, byte socks5Cmd, Message query, long endTime) { CompletableFuture cmdHandshakeF = new CompletableFuture<>(); CompletableFuture commandF = new CompletableFuture<>(); - // For CONNECT, DST.ADDR and DST.PORT are the address and port of the destination server. - // For UDP ASSOCIATE, DST.ADDR and DST.PORT are the address and port of the UDP client. - // If DST.ADDR and DST.PORT are set to 0.0.0.0:0, the proxy will accept UDP connections from any - // source address and port. - // After the first packet, the source address and port must not change. If they change, the - // proxy drops the connection and the UDP association. + // For CONNECT, DST.ADDR and DST.PORT represent the host and port of the remote address. + // For UDP ASSOCIATE, DST.ADDR and DST.PORT represent the destination of the returning UDP packets. + // If DST.ADDR and DST.PORT are set to 0.0.0.0:0, the proxy will send the packets back the same way. + // This allows it to work in a NAT-ed network. + // After the first packet, the source address and port must not change. If they do, the + // proxy will drop the connection and the UDP association. InetSocketAddress address = (socks5Cmd == SOCKS5_CMD_CONNECT) ? remoteAddress : new InetSocketAddress("0.0.0.0", 0); CmdRequest cmdRequest = new CmdRequest(socks5Cmd, address); From a12d0048e68f32e3d02922fb50857915c494a6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 17:48:05 +0100 Subject: [PATCH 36/53] improved comment for static registrationQueue and channelMap --- src/main/java/org/xbill/DNS/NioTcpHandler.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 06bd953d..956d02e9 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -25,9 +25,9 @@ @Slf4j @Getter public class NioTcpHandler extends NioClient { - // registrationQueue and channelMap must be static to be shared between instances - // otherwise, a second instance would overwrite the registration, timeout and close tasks of the - // first instance + // `registrationQueue` and `channelMap` must be static to be shared between instances. + // This is necessary because a second instance overwrites the registration, timeout, and close tasks, + // leaving no thread to process the pending registrations for the first instance. private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); private static final Map channelMap = new ConcurrentHashMap<>(); From a8be13b405a0ff0169f7ed3c416accd1ddcc574b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 17:54:57 +0100 Subject: [PATCH 37/53] improved comment for response to future mapping --- src/main/java/org/xbill/DNS/NioTcpHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 956d02e9..28da0505 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -222,7 +222,7 @@ private void processRead() { System.arraycopy( responseData.array(), responseData.arrayOffset(), data, 0, responseData.limit()); // the transactions for the socks5 handshake are synchronized - // you can assume that the responses are in order of the transactions in the queue + // you can assume that the responses are one after another for (Iterator it = pendingTransactions.iterator(); it.hasNext(); ) { Transaction t = it.next(); t.f.complete(data); From fe2a79e346952f12ce83c8b9c0912ca31588ff2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 21:44:11 +0100 Subject: [PATCH 38/53] added step for docker setup --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23d7f59e..f4c3f903 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,9 @@ jobs: distribution: temurin cache: maven + - name: Set up Docker + uses: docker/setup-docker-action@v4 + - name: Build with Maven shell: bash run: | From 73c0db0644863373c8ee04ae4fef85cf7dff317c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 21:55:35 +0100 Subject: [PATCH 39/53] set DOCKER_HOST --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4c3f903..c38e074a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,8 @@ jobs: - name: Set up Docker uses: docker/setup-docker-action@v4 + with: + set-host: true - name: Build with Maven shell: bash From 99455bec0892eab6c3548f0c850ff4b4c855ae18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 22:29:22 +0100 Subject: [PATCH 40/53] reduced logs for Testcontainers https://java.testcontainers.org/supported_docker_environment/logging_config/ --- src/test/resources/simplelogger.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties index 3894e75f..53552b10 100644 --- a/src/test/resources/simplelogger.properties +++ b/src/test/resources/simplelogger.properties @@ -3,3 +3,7 @@ org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z org.slf4j.simpleLogger.showDateTime=true org.slf4j.simpleLogger.log.org.xbill.DNS.Name=info org.slf4j.simpleLogger.log.org.xbill.DNS.Compression=info +org.slf4j.simpleLogger.log.org.testcontainers=INFO +org.slf4j.simpleLogger.log.tc=INFO +org.slf4j.simpleLogger.log.com.github.dockerjava=WARN +org.slf4j.simpleLogger.log.com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire=OFF From fb3f015a393f85d9592659122011b9b5801cef35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 22:36:49 +0100 Subject: [PATCH 41/53] fixed format violations --- .../java/org/xbill/DNS/NioSocksHandler.java | 15 ++++++--- .../java/org/xbill/DNS/NioTcpHandler.java | 31 ++++++++--------- .../org/xbill/DNS/io/SimpleSocksTest.java | 33 ++++++++++--------- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksHandler.java b/src/main/java/org/xbill/DNS/NioSocksHandler.java index 82730418..42f6ec61 100644 --- a/src/main/java/org/xbill/DNS/NioSocksHandler.java +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -86,7 +86,7 @@ public CompletableFuture doAuthHandshake( endTime, channel.getChannel(), methodSelectionF, - true); + true); channel.queueTransaction(methodSelectionTransaction); methodSelectionF.thenComposeAsync( @@ -129,7 +129,12 @@ private CompletableFuture handleUserPassAuth( UserPassAuthRequest userPassAuthRequest = new UserPassAuthRequest(socks5User, socks5Password); NioTcpHandler.Transaction userPwdAuthTransaction = new NioTcpHandler.Transaction( - query, userPassAuthRequest.toBytes(), endTime, channel.getChannel(), userPassAuthF, true); + query, + userPassAuthRequest.toBytes(), + endTime, + channel.getChannel(), + userPassAuthF, + true); channel.queueTransaction(userPwdAuthTransaction); userPassAuthF.thenComposeAsync( @@ -154,8 +159,10 @@ public CompletableFuture doSocks5Request( CompletableFuture cmdHandshakeF = new CompletableFuture<>(); CompletableFuture commandF = new CompletableFuture<>(); // For CONNECT, DST.ADDR and DST.PORT represent the host and port of the remote address. - // For UDP ASSOCIATE, DST.ADDR and DST.PORT represent the destination of the returning UDP packets. - // If DST.ADDR and DST.PORT are set to 0.0.0.0:0, the proxy will send the packets back the same way. + // For UDP ASSOCIATE, DST.ADDR and DST.PORT represent the destination of the returning UDP + // packets. + // If DST.ADDR and DST.PORT are set to 0.0.0.0:0, the proxy will send the packets back the same + // way. // This allows it to work in a NAT-ed network. // After the first packet, the source address and port must not change. If they do, the // proxy will drop the connection and the UDP association. diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 28da0505..cb204b58 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -26,7 +26,8 @@ @Getter public class NioTcpHandler extends NioClient { // `registrationQueue` and `channelMap` must be static to be shared between instances. - // This is necessary because a second instance overwrites the registration, timeout, and close tasks, + // This is necessary because a second instance overwrites the registration, timeout, and close + // tasks, // leaving no thread to process the pending registrations for the first instance. private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); private static final Map channelMap = new ConcurrentHashMap<>(); @@ -73,10 +74,10 @@ private void closeTcp() { registrationQueue.clear(); EOFException closing = new EOFException("Client is closing"); channelMap.forEach( - (key, state) -> { - state.handleTransactionException(closing); - state.handleChannelException(closing); - }); + (key, state) -> { + state.handleTransactionException(closing); + state.handleChannelException(closing); + }); channelMap.clear(); } @@ -114,28 +115,28 @@ boolean send() throws IOException { "TCP write: transaction id=" + query.getHeader().getID(), channel.socket().getLocalSocketAddress(), channel.socket().getRemoteSocketAddress(), - queryDataBuffer); + queryDataBuffer); while (queryDataBuffer.hasRemaining()) { long bytesWritten = channel.write(queryDataBuffer); bytesWrittenTotal += bytesWritten; if (bytesWritten == 0) { log.debug( - "Insufficient room for the data in the underlying output buffer for transaction {}, retrying", - query.getHeader().getID()); + "Insufficient room for the data in the underlying output buffer for transaction {}, retrying", + query.getHeader().getID()); return false; } else if (bytesWrittenTotal < queryData.length) { log.debug( - "Wrote {} of {} bytes data for transaction {}", - bytesWrittenTotal, - queryData.length, - query.getHeader().getID()); + "Wrote {} of {} bytes data for transaction {}", + bytesWrittenTotal, + queryData.length, + query.getHeader().getID()); } } log.debug( - "Send for transaction {} is complete, wrote {} bytes", - query.getHeader().getID(), - bytesWrittenTotal); + "Send for transaction {} is complete, wrote {} bytes", + query.getHeader().getID(), + bytesWrittenTotal); return true; } } diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java index 7281a1ad..45e26ec2 100644 --- a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -1,5 +1,11 @@ package org.xbill.DNS.io; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -8,19 +14,11 @@ import org.xbill.DNS.*; import org.xbill.DNS.Record; -import java.io.File; -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - public class SimpleSocksTest { - static final ComposeContainer environment = new ComposeContainer( - new File("src/test/resources/compose/compose.yml") - ) - .withBuild(true) - .waitingFor("dante-socks5", Wait.forHealthcheck()); + static final ComposeContainer environment = + new ComposeContainer(new File("src/test/resources/compose/compose.yml")) + .withBuild(true) + .waitingFor("dante-socks5", Wait.forHealthcheck()); @BeforeAll public static void setUp() throws IOException, InterruptedException { @@ -34,10 +32,15 @@ public static void tearDown() { environment.stop(); } - private SimpleResolver createResolver(String address, boolean useTCP, String user, String password) throws IOException { + private SimpleResolver createResolver( + String address, boolean useTCP, String user, String password) throws IOException { SimpleResolver res = address == null ? new SimpleResolver() : new SimpleResolver(address); - InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); - NioSocks5ProxyConfig config = user == null ? new NioSocks5ProxyConfig(proxyAddress) : new NioSocks5ProxyConfig(proxyAddress, user, password); + InetSocketAddress proxyAddress = + new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 1080); + NioSocks5ProxyConfig config = + user == null + ? new NioSocks5ProxyConfig(proxyAddress) + : new NioSocks5ProxyConfig(proxyAddress, user, password); res.setIoClientFactory(new NioSocks5ProxyFactory(config)); res.setTCP(useTCP); return res; From 938e726946a31d108d11cbdf5de6d6c25a815c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 22:43:23 +0100 Subject: [PATCH 42/53] fixed format violations --- src/test/java/org/xbill/DNS/io/SimpleSocksTest.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java index 45e26ec2..cf07ea8b 100644 --- a/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS.io; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -11,8 +12,14 @@ import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; -import org.xbill.DNS.*; +import org.xbill.DNS.DClass; +import org.xbill.DNS.Message; +import org.xbill.DNS.Name; +import org.xbill.DNS.NioSocks5ProxyConfig; +import org.xbill.DNS.NioSocks5ProxyFactory; import org.xbill.DNS.Record; +import org.xbill.DNS.SimpleResolver; +import org.xbill.DNS.Type; public class SimpleSocksTest { static final ComposeContainer environment = From b255bc3e1b834dc8e02d8a0580a9c61b6c3f4d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 22:48:34 +0100 Subject: [PATCH 43/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c38e074a..c5a50d84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,6 +48,7 @@ jobs: uses: docker/setup-docker-action@v4 with: set-host: true + tcp-port: 2375 - name: Build with Maven shell: bash From 10141bccebfbfc1314e77fc44893d7c55adae615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 22:57:01 +0100 Subject: [PATCH 44/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5a50d84..a56030db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,6 +49,10 @@ jobs: with: set-host: true tcp-port: 2375 + - name: Set different DOCKER_HOST on Windows + if: matrix.os == 'windows-latest' + run: | + setx DOCKER_HOST "tcp://localhost:2375" - name: Build with Maven shell: bash From 7caf0759a8877a1bb76a973546091753a67178d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 23:08:44 +0100 Subject: [PATCH 45/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a56030db..78ca41df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: - name: Set different DOCKER_HOST on Windows if: matrix.os == 'windows-latest' run: | - setx DOCKER_HOST "tcp://localhost:2375" + echo "DOCKER_HOST=tcp://localhost:2375" >> "$GITHUB_ENV" - name: Build with Maven shell: bash From fd301baa05b1f4e4358847abfc99840922fd6a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 23:28:34 +0100 Subject: [PATCH 46/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78ca41df..c72032e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,14 +49,13 @@ jobs: with: set-host: true tcp-port: 2375 - - name: Set different DOCKER_HOST on Windows - if: matrix.os == 'windows-latest' - run: | - echo "DOCKER_HOST=tcp://localhost:2375" >> "$GITHUB_ENV" - name: Build with Maven shell: bash run: | + if [ "${{ matrix.os }}" == "windows-latest" ]; then + setx DOCKER_HOST="tcp://localhost:2375" + fi TEST_EXCLUSIONS=$([ '${{ matrix.java }}' == '21' ] && echo "" || echo "concurrency" ) mvn verify \ -B \ From 3b62593ee28b474b9840d690165b9beadb588be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 23:30:33 +0100 Subject: [PATCH 47/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c72032e2..293d5865 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: shell: bash run: | if [ "${{ matrix.os }}" == "windows-latest" ]; then - setx DOCKER_HOST="tcp://localhost:2375" + setx DOCKER_HOST "tcp://localhost:2375" fi TEST_EXCLUSIONS=$([ '${{ matrix.java }}' == '21' ] && echo "" || echo "concurrency" ) mvn verify \ From b97610d471671945609ca2f88520dfd29b5baf18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 23:40:03 +0100 Subject: [PATCH 48/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 293d5865..1f804914 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,7 @@ jobs: run: | if [ "${{ matrix.os }}" == "windows-latest" ]; then setx DOCKER_HOST "tcp://localhost:2375" + setx TESTCONTAINERS_RYUK_DISABLED "true" fi TEST_EXCLUSIONS=$([ '${{ matrix.java }}' == '21' ] && echo "" || echo "concurrency" ) mvn verify \ From 4008b84a401a54eae10fc76850afe50a19522e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 23:53:30 +0100 Subject: [PATCH 49/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f804914..3472c954 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,10 +53,6 @@ jobs: - name: Build with Maven shell: bash run: | - if [ "${{ matrix.os }}" == "windows-latest" ]; then - setx DOCKER_HOST "tcp://localhost:2375" - setx TESTCONTAINERS_RYUK_DISABLED "true" - fi TEST_EXCLUSIONS=$([ '${{ matrix.java }}' == '21' ] && echo "" || echo "concurrency" ) mvn verify \ -B \ @@ -64,6 +60,8 @@ jobs: -"Dgpg.skip" \ -DexcludedGroups="${TEST_EXCLUSIONS}" \ jacoco:report + env: + DOCKER_HOST: tcp://localhost:2375 - name: Copy build reports shell: bash From d416209c9d1affb19f2966d5bb88af1219ccd7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 26 Jan 2025 23:59:45 +0100 Subject: [PATCH 50/53] try to make Testcontainers work with windows https://java.testcontainers.org/supported_docker_environment/windows/ --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3472c954..c38e074a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,6 @@ jobs: uses: docker/setup-docker-action@v4 with: set-host: true - tcp-port: 2375 - name: Build with Maven shell: bash @@ -60,8 +59,6 @@ jobs: -"Dgpg.skip" \ -DexcludedGroups="${TEST_EXCLUSIONS}" \ jacoco:report - env: - DOCKER_HOST: tcp://localhost:2375 - name: Copy build reports shell: bash From 7b37e197686c2e9a648670dba95a1654d2531e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Sun, 9 Feb 2025 14:54:35 +0100 Subject: [PATCH 51/53] removed differentiation of remote address for SocksUdpAssociateChannel + better error handling --- .../DNS/NioSocksUdpAssociateChannelPool.java | 15 +++++++-------- .../java/org/xbill/DNS/NioSocksUdpClient.java | 4 +++- .../java/org/xbill/DNS/NioTcpHandler.java | 19 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java index 1dca2182..718106f1 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -19,7 +19,7 @@ public class NioSocksUdpAssociateChannelPool { private final NioTcpHandler tcpHandler; private final NioUdpHandler udpHandler; - private final Map channelMap = new ConcurrentHashMap<>(); + private final static Map channelMap = new ConcurrentHashMap<>(); public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { this.tcpHandler = tcpHandler; @@ -29,13 +29,12 @@ public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler u public SocksUdpAssociateChannelState createOrGetSocketChannelState( InetSocketAddress local, InetSocketAddress remote, - NioSocksHandler proxy, CompletableFuture future) { String key = local + " " + remote; SocksUdpAssociateChannelGroup group = channelMap.computeIfAbsent( key, k -> new SocksUdpAssociateChannelGroup(tcpHandler, udpHandler)); - return group.createOrGetDatagramChannel(local, remote, proxy, future); + return group.createOrGetDatagramChannel(local, remote, future); } public void removeIdleChannels() { @@ -60,7 +59,7 @@ private static class SocksUdpAssociateChannelGroup { private final Queue channels; private final NioTcpHandler tcpHandler; private final NioUdpHandler udpHandler; - private final int defaultChannelIdleTimeout = 1000; + private final int defaultChannelIdleTimeout = 60000; public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { channels = new ConcurrentLinkedQueue<>(); @@ -71,13 +70,12 @@ public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udp public SocksUdpAssociateChannelState createOrGetDatagramChannel( InetSocketAddress local, InetSocketAddress remote, - NioSocksHandler proxy, CompletableFuture future) { SocksUdpAssociateChannelState channelState = null; for (Iterator it = channels.iterator(); it.hasNext(); ) { SocksUdpAssociateChannelState c = it.next(); synchronized (c) { - if (!c.isOccupied) { + if (!c.isOccupied && !c.isFailed()) { channelState = c; c.occupy(); break; @@ -88,7 +86,7 @@ public SocksUdpAssociateChannelState createOrGetDatagramChannel( if (channelState == null) { try { SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); - newChannel.tcpChannel = tcpHandler.createChannelState(local, remote, proxy, future); + newChannel.tcpChannel = tcpHandler.createChannelState(local, remote, future); newChannel.udpChannel = udpHandler.createChannel(local, future); newChannel.poolChannelIdleTimeout = System.currentTimeMillis() + defaultChannelIdleTimeout; @@ -104,7 +102,7 @@ public SocksUdpAssociateChannelState createOrGetDatagramChannel( } public void removeChannelState(SocksUdpAssociateChannelState channel) throws IOException { - if (channel.occupy()) { + if (channel.occupy() || channel.isFailed()) { channels.remove(channel); channel.tcpChannel.close(); channel.udpChannel.close(); @@ -119,6 +117,7 @@ public static class SocksUdpAssociateChannelState { private NioTcpHandler.ChannelState tcpChannel; private DatagramChannel udpChannel; private boolean isOccupied = false; + private boolean isFailed = false; private long poolChannelIdleTimeout; public synchronized boolean occupy() { diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index fb6ea69c..69bb2ac4 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -34,7 +34,7 @@ public CompletableFuture sendAndReceiveUdp( socksConfig.getSocks5User(), socksConfig.getSocks5Password()); NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = - udpHandler.getUdpPool().createOrGetSocketChannelState(local, remote, proxy, f); + udpHandler.getUdpPool().createOrGetSocketChannelState(local, proxy.getProxyAddress(), f); synchronized (channel) { if (channel.getTcpChannel().socks5HandshakeF == null @@ -70,6 +70,7 @@ public CompletableFuture sendAndReceiveUdp( }) .exceptionally( ex -> { + channel.setFailed(true); f.completeExceptionally(ex); return null; }); @@ -77,6 +78,7 @@ public CompletableFuture sendAndReceiveUdp( }) .exceptionally( ex -> { + channel.setFailed(true); f.completeExceptionally(ex); return null; }); diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index cb204b58..b6f01be8 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -345,7 +345,6 @@ public void dnsTransaction( public ChannelState createChannelState( InetSocketAddress local, InetSocketAddress remote, - NioSocksHandler proxy, CompletableFuture f) { log.debug("Opening async channel for l={}/r={}", local, remote); SocketChannel c = null; @@ -355,12 +354,7 @@ public ChannelState createChannelState( if (local != null) { c.bind(local); } - - if (proxy != null) { - c.connect(proxy.getProxyAddress()); - } else { - c.connect(remote); - } + c.connect(remote); return new ChannelState(c); } catch (IOException e) { if (c != null) { @@ -378,10 +372,9 @@ public ChannelState createChannelState( public ChannelState createOrGetChannelState( InetSocketAddress local, InetSocketAddress remote, - NioSocksHandler proxy, CompletableFuture f) { return channelMap.computeIfAbsent( - new ChannelKey(local, remote), key -> createChannelState(local, remote, proxy, f)); + new ChannelKey(local, remote), key -> createChannelState(local, remote, f)); } public CompletableFuture sendAndReceiveTcp( @@ -393,7 +386,13 @@ public CompletableFuture sendAndReceiveTcp( Duration timeout) { CompletableFuture f = new CompletableFuture<>(); - ChannelState channel = createOrGetChannelState(local, remote, proxy, f); + InetSocketAddress remoteAddr; + if (proxy != null) { + remoteAddr = proxy.getProxyAddress(); + } else { + remoteAddr = remote; + } + ChannelState channel = createOrGetChannelState(local, remoteAddr, f); if (channel != null) { log.trace( "Creating transaction for id {} ({}/{})", From d8dde40cb79d693dadb0cad6914955e3a4df6b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Mon, 10 Feb 2025 11:34:05 +0100 Subject: [PATCH 52/53] fixed style --- .../DNS/NioSocksUdpAssociateChannelPool.java | 20 +++++++++---------- .../java/org/xbill/DNS/NioSocksUdpClient.java | 5 +++-- .../java/org/xbill/DNS/NioTcpHandler.java | 19 ++++++++---------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java index 718106f1..4d17d791 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -1,6 +1,11 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.DatagramChannel; @@ -10,16 +15,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; @Slf4j public class NioSocksUdpAssociateChannelPool { private final NioTcpHandler tcpHandler; private final NioUdpHandler udpHandler; - private final static Map channelMap = new ConcurrentHashMap<>(); + private static final Map channelMap = + new ConcurrentHashMap<>(); public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { this.tcpHandler = tcpHandler; @@ -27,9 +29,7 @@ public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler u } public SocksUdpAssociateChannelState createOrGetSocketChannelState( - InetSocketAddress local, - InetSocketAddress remote, - CompletableFuture future) { + InetSocketAddress local, InetSocketAddress remote, CompletableFuture future) { String key = local + " " + remote; SocksUdpAssociateChannelGroup group = channelMap.computeIfAbsent( @@ -68,9 +68,7 @@ public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udp } public SocksUdpAssociateChannelState createOrGetDatagramChannel( - InetSocketAddress local, - InetSocketAddress remote, - CompletableFuture future) { + InetSocketAddress local, InetSocketAddress remote, CompletableFuture future) { SocksUdpAssociateChannelState channelState = null; for (Iterator it = channels.iterator(); it.hasNext(); ) { SocksUdpAssociateChannelState c = it.next(); diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index 69bb2ac4..6905b786 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -1,11 +1,12 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.UdpIoClient; + import java.net.InetSocketAddress; import java.time.Duration; import java.util.concurrent.CompletableFuture; -import lombok.extern.slf4j.Slf4j; -import org.xbill.DNS.io.UdpIoClient; @Slf4j final class NioSocksUdpClient extends NioClient implements UdpIoClient { diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index b6f01be8..980528c3 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -1,6 +1,12 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; @@ -16,11 +22,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; @Slf4j @Getter @@ -343,9 +344,7 @@ public void dnsTransaction( } public ChannelState createChannelState( - InetSocketAddress local, - InetSocketAddress remote, - CompletableFuture f) { + InetSocketAddress local, InetSocketAddress remote, CompletableFuture f) { log.debug("Opening async channel for l={}/r={}", local, remote); SocketChannel c = null; try { @@ -370,9 +369,7 @@ public ChannelState createChannelState( } public ChannelState createOrGetChannelState( - InetSocketAddress local, - InetSocketAddress remote, - CompletableFuture f) { + InetSocketAddress local, InetSocketAddress remote, CompletableFuture f) { return channelMap.computeIfAbsent( new ChannelKey(local, remote), key -> createChannelState(local, remote, f)); } From be7245c438e392c8514784e69f00e119dbed96bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9F?= Date: Mon, 10 Feb 2025 12:19:59 +0100 Subject: [PATCH 53/53] fixed style --- .../xbill/DNS/NioSocksUdpAssociateChannelPool.java | 9 ++++----- src/main/java/org/xbill/DNS/NioSocksUdpClient.java | 5 ++--- src/main/java/org/xbill/DNS/NioTcpHandler.java | 11 +++++------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java index 4d17d791..8800efcf 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -1,11 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.DatagramChannel; @@ -15,6 +10,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; @Slf4j public class NioSocksUdpAssociateChannelPool { diff --git a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java index 6905b786..69bb2ac4 100644 --- a/src/main/java/org/xbill/DNS/NioSocksUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -1,12 +1,11 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.extern.slf4j.Slf4j; -import org.xbill.DNS.io.UdpIoClient; - import java.net.InetSocketAddress; import java.time.Duration; import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.io.UdpIoClient; @Slf4j final class NioSocksUdpClient extends NioClient implements UdpIoClient { diff --git a/src/main/java/org/xbill/DNS/NioTcpHandler.java b/src/main/java/org/xbill/DNS/NioTcpHandler.java index 980528c3..05598be6 100644 --- a/src/main/java/org/xbill/DNS/NioTcpHandler.java +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -1,12 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; @@ -22,6 +16,11 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; @Slf4j @Getter