diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23d7f59e..c38e074a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,11 @@ jobs: distribution: temurin cache: maven + - name: Set up Docker + uses: docker/setup-docker-action@v4 + with: + set-host: true + - name: Build with Maven shell: bash run: | diff --git a/pom.xml b/pom.xml index e033b92f..434faebc 100644 --- a/pom.xml +++ b/pom.xml @@ -685,6 +685,13 @@ 0.16 test + + org.testcontainers + testcontainers + 1.20.4 + test + + diff --git a/src/main/java/org/xbill/DNS/NioSocks5ProxyConfig.java b/src/main/java/org/xbill/DNS/NioSocks5ProxyConfig.java new file mode 100644 index 00000000..e1e24c9a --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocks5ProxyConfig.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +import java.net.InetSocketAddress; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class NioSocks5ProxyConfig { + private InetSocketAddress proxyAddress; + private AuthMethod authMethod; + private String socks5User; + private String socks5Password; + + public enum AuthMethod { + NONE, + GSSAPI, + USER_PASS + } + + public NioSocks5ProxyConfig(InetSocketAddress proxyAddress) { + this(proxyAddress, null, null); + authMethod = AuthMethod.NONE; + } + + 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); + // } +} 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..d6707633 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocks5ProxyFactory.java @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +import java.util.Objects; +import lombok.Getter; +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 { + private final NioSocks5ProxyConfig config; + private final TcpIoClient tcpIoClient; + private final UdpIoClient udpIoClient; + + 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; + } + + @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..42f6ec61 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocksHandler.java @@ -0,0 +1,460 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +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; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Getter +@Slf4j +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; + 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<>(); + CompletableFuture methodSelectionF = new CompletableFuture<>(); + MethodSelectionRequest methodSelectionRequest = getMethodSelectionRequest(); + NioTcpHandler.Transaction methodSelectionTransaction = + new NioTcpHandler.Transaction( + query, + methodSelectionRequest.toBytes(), + endTime, + channel.getChannel(), + methodSelectionF, + true); + 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; + }); + + return 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, + true); + 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; + }); + + return authHandshakeF; + } + + 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 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); + NioTcpHandler.Transaction commandTransaction = + new NioTcpHandler.Transaction( + query, cmdRequest.toBytes(), endTime, channel.getChannel(), commandF, true); + 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; + }); + + 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 || 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; + } + + public byte[] addUdpHeader(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 + 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) throws IllegalArgumentException { + if (in.length < 10) { + throw new IllegalArgumentException("SOCKS5 UDP response too short"); + } + + 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; + } + + 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 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(); + } + } + + 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) { + this.version = SOCKS5_VERSION; + this.command = command; + this.reserved = SOCKS5_RESERVED; + if (address.getAddress() instanceof Inet4Address) { + this.addressType = SOCKS5_ATYP_IPV4; + this.addressBytes = address.getAddress().getAddress(); + this.bufferSize = 10; + } else if (address.getAddress() instanceof Inet6Address) { + this.addressType = SOCKS5_ATYP_IPV6; + this.addressBytes = address.getAddress().getAddress(); + this.bufferSize = 22; + } else { + this.addressType = SOCKS5_ATYP_DOMAINNAME; + this.addressBytes = address.getHostName().getBytes(StandardCharsets.UTF_8); + this.bufferSize = 6 + 1 + addressBytes.length; + } + this.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); + if (addressType == SOCKS5_ATYP_DOMAINNAME) { + buffer.put((byte) addressBytes.length); + } + 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); + 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 new file mode 100644 index 00000000..bde2b62f --- /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 java.net.InetSocketAddress; +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 { + private final NioTcpHandler tcpHandler; + private final NioSocks5ProxyConfig socksConfig; + + NioSocksTcpClient(NioSocks5ProxyConfig 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, + 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 new file mode 100644 index 00000000..8800efcf --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocksUdpAssociateChannelPool.java @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.DatagramChannel; +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 { + private final NioTcpHandler tcpHandler; + private final NioUdpHandler udpHandler; + private static final Map channelMap = + new ConcurrentHashMap<>(); + + public NioSocksUdpAssociateChannelPool(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { + this.tcpHandler = tcpHandler; + this.udpHandler = udpHandler; + } + + public SocksUdpAssociateChannelState createOrGetSocketChannelState( + InetSocketAddress local, InetSocketAddress remote, CompletableFuture future) { + String key = local + " " + remote; + SocksUdpAssociateChannelGroup group = + channelMap.computeIfAbsent( + key, k -> new SocksUdpAssociateChannelGroup(tcpHandler, udpHandler)); + return group.createOrGetDatagramChannel(local, remote, 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 Queue channels; + private final NioTcpHandler tcpHandler; + private final NioUdpHandler udpHandler; + private final int defaultChannelIdleTimeout = 60000; + + public SocksUdpAssociateChannelGroup(NioTcpHandler tcpHandler, NioUdpHandler udpHandler) { + channels = new ConcurrentLinkedQueue<>(); + this.tcpHandler = tcpHandler; + this.udpHandler = udpHandler; + } + + public SocksUdpAssociateChannelState createOrGetDatagramChannel( + InetSocketAddress local, InetSocketAddress remote, CompletableFuture future) { + SocksUdpAssociateChannelState channelState = null; + for (Iterator it = channels.iterator(); it.hasNext(); ) { + SocksUdpAssociateChannelState c = it.next(); + synchronized (c) { + if (!c.isOccupied && !c.isFailed()) { + channelState = c; + c.occupy(); + break; + } + } + } + + if (channelState == null) { + try { + SocksUdpAssociateChannelState newChannel = new SocksUdpAssociateChannelState(); + newChannel.tcpChannel = tcpHandler.createChannelState(local, remote, 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() || channel.isFailed()) { + channels.remove(channel); + channel.tcpChannel.close(); + channel.udpChannel.close(); + } + } + } + + @RequiredArgsConstructor + @Getter + @Setter + 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() { + 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 new file mode 100644 index 00000000..69bb2ac4 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioSocksUdpClient.java @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS; + +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 NioSocks5ProxyConfig socksConfig; + + NioSocksUdpClient(NioSocks5ProxyConfig 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, + socksConfig.getSocks5User(), + socksConfig.getSocks5Password()); + NioSocksUdpAssociateChannelPool.SocksUdpAssociateChannelState channel = + udpHandler.getUdpPool().createOrGetSocketChannelState(local, proxy.getProxyAddress(), f); + + 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 + .thenApplyAsync( + cmdBytes -> { + byte[] wrappedData = proxy.addUdpHeader(data); + NioSocksHandler.CmdResponse cmd = new NioSocksHandler.CmdResponse(cmdBytes); + InetSocketAddress newRemote = + new InetSocketAddress(socksConfig.getProxyAddress().getAddress(), cmd.getPort()); + 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 -> { + channel.setFailed(true); + f.completeExceptionally(ex); + return null; + }); + return null; + }) + .exceptionally( + ex -> { + channel.setFailed(true); + 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..90ed4473 100644 --- a/src/main/java/org/xbill/DNS/NioTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioTcpClient.java @@ -1,288 +1,18 @@ // 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 +22,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..05598be6 --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioTcpHandler.java @@ -0,0 +1,429 @@ +// 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.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. + // 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<>(); + + 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); + state.handleChannelException(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 final boolean isSocks5; + 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) { + 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( + "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 + @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; + 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); + close(); + } + + public void close() { + 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 one after another + 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 { + 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); + } + + 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) { + channel.setSocks5(false); + Transaction t = new Transaction(query, data, endTime, channel.channel, f, false); + channel.queueTransaction(t); + } + + public ChannelState createChannelState( + InetSocketAddress local, InetSocketAddress remote, 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); + } + 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, CompletableFuture f) { + return channelMap.computeIfAbsent( + new ChannelKey(local, remote), key -> createChannelState(local, remote, f)); + } + + public CompletableFuture sendAndReceiveTcp( + InetSocketAddress local, + InetSocketAddress remote, + NioSocksHandler proxy, + Message query, + byte[] data, + Duration timeout) { + CompletableFuture f = new CompletableFuture<>(); + + 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 {} ({}/{})", + 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()) { + channel.setSocks5(true); + channel.socks5HandshakeF = + proxy.doSocks5Handshake( + channel, NioSocksHandler.SOCKS5_CMD_CONNECT, query, endTime); + } + } + // Chain the SOCKS5 transactions with the DNS data transaction + channel + .socks5HandshakeF + .thenRunAsync( + () -> { + dnsTransaction(channel, query, data, endTime, f); + }) + .exceptionally( + ex -> { + f.completeExceptionally(ex); + return null; + }); + } else { + 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..87d28617 100644 --- a/src/main/java/org/xbill/DNS/NioUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioUdpClient.java @@ -1,161 +1,18 @@ // 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; @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 +23,6 @@ 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; - } - - 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(); + 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 new file mode 100644 index 00000000..01f2f4fc --- /dev/null +++ b/src/main/java/org/xbill/DNS/NioUdpHandler.java @@ -0,0 +1,271 @@ +// 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.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +final class NioUdpHandler extends NioClient { + private final int ephemeralStart; + private final int ephemeralRange; + private final SecureRandom prng; + 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; + + 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(); + } + + // NioTcpHandler for SOCKS5 UDP associate + udpPool = new NioSocksUdpAssociateChannelPool(new NioTcpHandler(), this); + + 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(); + } + } + + udpPool.removeIdleChannels(); + } + + 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; + 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. + 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, 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; + } + + 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 = (channel != null); + if (channel == null) { + 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); + + final Selector selector = selector(); + pendingTransactions.add(t); + registrationQueue.add(t); + selector.wakeup(); + } catch (IOException e) { + silentCloseChannel(channel); + f.completeExceptionally(e); + } catch (Throwable e) { + silentCloseChannel(channel); + throw e; + } + + return f; + } +} 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..cf07ea8b --- /dev/null +++ b/src/test/java/org/xbill/DNS/io/SimpleSocksTest.java @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: BSD-3-Clause +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; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +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 = + 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 { + environment.start(); + // wait to make sure the container is ready + Thread.sleep(100); + } + + @AfterAll + public static void tearDown() { + 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 = + 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("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); + assertNotNull(response); + } + + @Test + public void testTCP() throws IOException { + 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); + assertNotNull(response); + } + + @Test + public void testAuth() throws IOException { + 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); + assertNotNull(response); + } +} diff --git a/src/test/resources/compose/.env b/src/test/resources/compose/.env new file mode 100644 index 00000000..3d970a2c --- /dev/null +++ b/src/test/resources/compose/.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/resources/compose/Dockerfile b/src/test/resources/compose/Dockerfile new file mode 100644 index 00000000..f3bff7bb --- /dev/null +++ b/src/test/resources/compose/Dockerfile @@ -0,0 +1,39 @@ +# 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 \ + bind-tools \ + 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 \ + curl \ + linux-pam + +COPY sockd.conf /etc/ +COPY entrypoint.sh / + +EXPOSE 1080 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["sockd"] diff --git a/src/test/resources/compose/compose.yml b/src/test/resources/compose/compose.yml new file mode 100644 index 00000000..9c0636d4 --- /dev/null +++ b/src/test/resources/compose/compose.yml @@ -0,0 +1,43 @@ + +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 + env_file: + - .env + ports: + - "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 socks5://127.0.0.1:1080 http://10.5.0.2:8080/health" + 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/resources/compose/coredns/corefile b/src/test/resources/compose/coredns/corefile new file mode 100644 index 00000000..deeb53c1 --- /dev/null +++ b/src/test/resources/compose/coredns/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/resources/compose/coredns/test.db b/src/test/resources/compose/coredns/test.db new file mode 100644 index 00000000..11ed72ec --- /dev/null +++ b/src/test/resources/compose/coredns/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 diff --git a/src/test/resources/compose/entrypoint.sh b/src/test/resources/compose/entrypoint.sh new file mode 100755 index 00000000..440154ef --- /dev/null +++ b/src/test/resources/compose/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/resources/compose/sockd.conf b/src/test/resources/compose/sockd.conf new file mode 100644 index 00000000..178c0e3f --- /dev/null +++ b/src/test/resources/compose/sockd.conf @@ -0,0 +1,34 @@ +# logging +errorlog: /var/log/sockd.errlog +logoutput: /var/log/sockd.log + +# server address specification +internal: 0.0.0.0 port = 1080 +external: eth0 + +# auth +user.notprivileged: nobody +socksmethod: none username # "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 username # "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/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