| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
I haven't forgotten about this, but as mentioned, it'll take some more time until I can review it properly. |
Sorry, something went wrong.
|
No worries, take your time. Thanks for the update. |
Sorry, something went wrong.
|
I'll review in more detail later, but as a first change, please undo the new *sendAndReceive methods in the interfaces and their corresponding usage in SimpleResolver (as well as the new proxy constructor there) and DefaultIoClient. Instead, please create a dedicated IoClientFactory, e.g. Socks5ProxyIoClientFactory and Socks5ProxyIoClient, this can then internally handle the proxy. |
Sorry, something went wrong.
|
|
||
| c.connect(remote); | ||
| if (proxy != null) { | ||
| c.configureBlocking(true); |
There was a problem hiding this comment.
This is too easy :-)
The whole process of connecting to the proxy must be async here too. If this method starts to block, the entire thread pool can get exhausted and all the timeouts won't work anymore.
Sorry, something went wrong.
There was a problem hiding this comment.
Yes, I probably need to implement it as KeyProcessor. I would like to use the Transaction class from NioTcpClient and make it a separate public class.
Sorry, something went wrong.
| if (local != null) { | ||
| tcpChannel.bind(local); | ||
| } | ||
| tcpChannel.connect(proxy.getProxyAddress()); |
There was a problem hiding this comment.
Here too, this needs to be fully async. Also, this creates a dedicated TCP connection to the proxy for each request. Shouldn't there be just one (or maybe one per remote) and reused?
Sorry, something went wrong.
There was a problem hiding this comment.
You need only one open TCP connection to establish the UDP relay.
It might be optimal to have just one or a pool off connections.
Sorry, something went wrong.
| if (!state.channel.isConnected()) { | ||
| state.channel.register(selector, SelectionKey.OP_CONNECT, state); | ||
| } else { | ||
| if (state.channel.keyFor(selector) == null) { |
There was a problem hiding this comment.
What did you encounter that this is necessary? Also, would it be possible to merge the condition with the outer if?
Sorry, something went wrong.
There was a problem hiding this comment.
I encountered:
Exception in thread "dnsjava NIO selector" java.lang.NullPointerException: Cannot invoke "java.nio.channels.SelectionKey.interestOps(int)" because the return value of "java.nio.channels.SocketChannel.keyFor(java.nio.channels.Selector)" is null at org.dnsjava/org.xbill.DNS.NioTcpClient.processPendingRegistrations(NioTcpClient.java:47) at org.dnsjava/org.xbill.DNS.NioClient.runTasks(NioClient.java:168) at org.dnsjava/org.xbill.DNS.NioClient.runSelector(NioClient.java:133) at java.base/java.lang.Thread.run(Thread.java:1583) Exception in thread "main" java.io.IOException: Timed out while trying to resolve google.com./A, id=25831 at org.dnsjava/org.xbill.DNS.Resolver.send(Resolver.java:172) at org.dnsjava/org.xbill.DNS.tools.dig.main(dig.java:227) Caused by: java.util.concurrent.TimeoutException at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1960) at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2095) at org.dnsjava/org.xbill.DNS.Resolver.send(Resolver.java:153) ... 1 more
The merge with the outer condition didn't work out for me.
Sorry, something went wrong.
|
|
||
| private void writeToChannel(SocketChannel c, ByteBuffer buffer) { | ||
| try { | ||
| c.write(buffer); |
There was a problem hiding this comment.
Return the return value, then handle it everywhere the write might have failed but not thrown.
Sorry, something went wrong.
|
|
||
| private void readFromChannel(SocketChannel c, ByteBuffer buffer) { | ||
| try { | ||
| c.read(buffer); |
There was a problem hiding this comment.
Return the return value and handle it everywhere
Sorry, something went wrong.
| return new InetSocketAddress(this.getProxyAddress().getAddress(), port); | ||
| } | ||
|
|
||
| public byte[] addUdpHeader(byte[] in, InetSocketAddress to) { |
There was a problem hiding this comment.
Handle IPv6 in this method
Sorry, something went wrong.
There was a problem hiding this comment.
With this IPv6 worked for me
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);
}
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) remote.getPort()); // DST.PORT
buffer.put(data); // DATA
Sorry, something went wrong.
| return buffer.array(); | ||
| } | ||
|
|
||
| public byte[] removeUdpHeader(byte[] in) { |
There was a problem hiding this comment.
Handle IPv6
Sorry, something went wrong.
There was a problem hiding this comment.
It keeps track of the headerLength in case of IPv4, IPv6 or domainname and removes it accordingly
Sorry, something went wrong.
| buffer.put(SOCKS5_VERSION); | ||
| buffer.put(SOCKS5_CMD_CONNECT); | ||
| buffer.put(SOCKS5_RESERVED); | ||
| buffer.put(SOCKS5_ATYP_IPV4); |
There was a problem hiding this comment.
Handle IPv6; and remote could also be a hostname. What if the proxy should resolve the hostname because the client isn't even able to?
Sorry, something went wrong.
There was a problem hiding this comment.
I adjusted it to
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());
Sorry, something went wrong.
| 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); |
There was a problem hiding this comment.
Java has built-in GSSAPI support. Would be nice to include the support here too, or at least prepare the code flow to be extendable.
Sorry, something went wrong.
There was a problem hiding this comment.
I tried to use org.ietf.jgss, but I'm getting
Field gssCredential: Undefined reference: org.ietf.jgss.GSSCredential Undefined reference: org.ietf.jgss.GSSCredential
when running mvn clean package
Adding requires java.security.jgss; to the module-info.java didn't help.
Do you have an idea, why this is happening?
Sorry, something went wrong.
| import java.util.Objects; | ||
|
|
||
| @Getter | ||
| public class Socks5Proxy { |
There was a problem hiding this comment.
I'm not sure if there's a need for timeout handling?
Sorry, something went wrong.
There was a problem hiding this comment.
You can set timeouts on the Dante config. It should reply with "TTL expired" then. Don't know, how to do this properly. There might be the need to track and handle this. Or you add a retry mechanism.
Sorry, something went wrong.
|
I undid the changes in SimpleResolver, DefaultIoClient and *sendAndReceive. I also added a Socks5ProxyIoClientFactory. Now, you can set it up like following: InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("<socks5-host>"), 1080);
String socks5User = "<user>";
String socks5Password = "<password>";
Socks5ProxyConfig config = new Socks5ProxyConfig(proxyAddress, socks5User, socks5Password);
res.setIoClientFactory(new Socks5ProxyIoClientFactory(config));
I couldn't figure out how to add the SOCKS5 transactions with the NioClient. I implemented it like an event loop in the Socks5ProxyIoClientFactory and added the read and write operations in the attachment of the channel keys. I tried to implement the timeout handling with a ScheduledExecutorService, but I think it is not ready, yet. Now it's fully async. IPv6 worked with 2001:4860:4860::8888 for me. It also reuses the connection without repeating the SOCKS5 handshake again. |
Sorry, something went wrong.
|
Hi @ibauersachs. I suggested a huge amount of code additions and I also have changed a lot since the requested changes. I think, I should give some more descriptions. Initially, I did not understand the NioClient back then. So, I attempted to implement it in a way that suited the handling of the SOCKS5 handshake. My concept was to start with an event loop that uses the Selector as thread safe queue and store the read/write logic as Runnables in the attachment of the SelectionKey. A new Runnable is set in the attachment at the end of the previous one to chain the Runnables in the right order. Or the CompletableFuture is completed. It should also work together with a worker pool to execute the Runnables in a multithreaded way. Then later I realized I could not sync up the close operations for the implementation of timeouts into the event loop through the Selector. I didn't want to use selector.select(timeout) and opted for a solution with the java.util.concurrent.ScheduledExecutorService. I think, there is still a problem with synchronizing the close operation with the connect, read and write operations in a thread safe way. Now I think, I should write a method that handles connect, read and write and wrap it around a synchronized(tcpSelectionKey) block in Socks5ProxyTcpIoClient. Similar for Socks5ProxyUdpIoClient. To reuse the connections and avoiding the repetition of the SOCKS handshake, I defined private static final Map<String, Map<String, Socks5Proxy>> socks5ConnectionPool = new ConcurrentHashMap<>(); Initially, I used a simple Map<String, Socks5Proxy>, but I realized that I needed to track the state of the TCP connection to prevent the SOCKS handshake and read/write operations from being disrupted by concurrent usage. Therefore, I decided to use a nested Map structure to track the connections per remote address in the outer Map and manage multiple connections for the same remote host in a the inner Map. This structure might require a cleanup strategy to close connections that aren't used for a longer time. I used the SelectionKey registration and deregistration from the Selector to track whether a connection is usable or if a new connection to the host needs to be established. This way I prevent that concurrent use messes up the UDP/TCP transaction of a connection. What do you think? |
Sorry, something went wrong.
|
Hi, sorry for not getting back to you yet. I haven't had time to thoroughly review this PR again, and as you probably figured out, the NIO stuff is nasty. At first glance, there's a lot of duplication between the existing IO client classes and what you wrote now. I wonder if there couldn't be more shared code, especially the selector thread and timeout handling. The IO clients are currently package-private, so there can be changes to the API if needed. It would also be good if there were some unit/integration tests. You could use Testcontainers to run a real proxy to test against. |
Sorry, something went wrong.
|
Hi. Thanks for your quick reply. Yes. I now believe that the logic for the SOCKS proxy should be built on top of the NioClients. I can't use the current NioTcpClient because of this part ChannelState channel = channelMap.computeIfAbsent(new ChannelKey(local, remote), ...). For SOCKS, the remote would always be the proxy. |
Sorry, something went wrong.
|
The current TcpIoClient implementation currently makes a hard assumption that it's talking directly with DNS servers, also with the prefix-length expected in each reply. You can also try introducing an intermediate class if it helps: NioClient
|
AbstractTcpClient
| |
DnsTcpClient SocksClient
Going back to the IoClientFactory will provide the challenge of keeping the public interface stable. |
Sorry, something went wrong.
|
That might help. I try it out. |
Sorry, something went wrong.
|
Hallo @ibauersachs. Thanks for your input. I appreciate it. I integrated the transactions with the IoClients. Selector and timeout handling is now shared. Also I added 3 basic tests with Testcontainers (ComposeContainer). The builds for this pull request are currently failing. Probably, Docker is not set up in the build containers. Error: Errors: Error: org.xbill.DNS.io.SimpleSocksTest.null Error: Run 1: SimpleSocksTest.setUp:27 � IllegalState Could not find a valid Docker environment. Please see logs and check configuration Error: Run 2: SimpleSocksTest.setUp:27 � ContainerFetch Can't get Docker image: RemoteDockerImage(imageName=docker:24.0.2, imagePullPolicy=DefaultPullPolicy(), It works fine on my server. [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 38.84 s -- in org.xbill.DNS.io.SimpleSocksTest It seems that on Mac, there is an issue with routing UDP traffic into the Docker container. I do not believe this is related to the proper functioning of the SOCKS interactions. [ERROR] Errors: [ERROR] org.xbill.DNS.io.SimpleSocksTest.testAuth [ERROR] Run 1: SimpleSocksTest.testAuth » PortUnreachable It works fine when I try it from a test container in the same Docker network. The introduction of intermediate classes worked well. NioClient
/ \
NioTcpHandler NioUdpHandler
/ | \ / \
NioTcpClient NioSocksTcpClient NioSocksUdpClient NioUdpClient
\ /
NioSocksHandler
For the NioSocksHandler, I now handle I/O using futures and the provided Transaction classes. I use a future for the entire SOCKS handshake and one for each transaction. For the NioSocksTcpClient, I moved the logic from NioTcpClient to the NioTcpHandler. I introduced boolean attributes to perform operations for the SOCKS interactions when necessary. I added a future as an attribute for the SOCKS handshake on the ChannelState to manage the reuse of the TCP channels without redoing the SOCKS handshake. For the NioSocksUdpClient, I encountered an issue where the SOCKS proxy only accepts UDP interactions from the same port after the first interaction. To address this, I wrote NioSocksUdpAssociateChannelPool, that keeps track of the established SOCKS UDP associations and handles multiple SOCKS connections per local+remote address. I plan to add more tests and further improve and simplify the NioSocksUdpAssociateChannelPool to avoid needing a new SOCKS connection for each remote address. And I'm looking for a way to reduce the log output of Testcontainers. It would be nice to get a short review and your opinion on the pull request. |
Sorry, something went wrong.
|
Thanks! I'll need a while to dig through all of this. As for the integration tests, you could add a tag to only run them on the stage that also runs Sonar (i.e. Ubuntu and currently Java 21). Docker might need to be installed beforehand, you can do that as an actions step with apt-get install. Also, can you please rebase the branch on top of master? |
Sorry, something went wrong.
|
I rebased the branch on top of master. |
Sorry, something went wrong.
…l + better error handling
| Back | FazBrowse Home | New Git URL |
I added SOCKS5 support to the ioClients.
It does work for TCP and UDP (udp associate).
It also supports user/pwd authentication.
The implementation is based on
I tested it on a Dante SOCKS5 proxy with following config.
# 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.privileged: root user.notprivileged: nobody socksmethod: none # "none" or "username" user/pwd auth # 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 } # 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 auth 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 }You should be able to install the Dante SOCKS5 proxy via package managers like apt, apk or brew.
I can also provide you a Docker setup.
I tried it like
InetSocketAddress localAddress = new InetSocketAddress(InetAddress.getByName("0.0.0.0"), 0); InetSocketAddress remoteAddress = new InetSocketAddress(InetAddress.getByName("1.1.1.1"), 53); InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("<socks5-host>"), 1080); Socks5Proxy proxy = new Socks5Proxy(proxyAddress, remoteAddress, localAddress); res = new SimpleResolver(proxy); res.setTCP(true); // optionalor
InetSocketAddress localAddress = new InetSocketAddress(InetAddress.getByName("0.0.0.0"), 0); InetSocketAddress remoteAddress = new InetSocketAddress(InetAddress.getByName("1.1.1.1"), 53); InetSocketAddress proxyAddress = new InetSocketAddress(InetAddress.getByName("<socks5-host>"), 1080); String socks5User = "<user>"; String socks5Password = "<password>"; Socks5Proxy proxy = new Socks5Proxy(proxyAddress, remoteAddress, localAddress, socks5User, socks5Password); res = new SimpleResolver(proxy); res.setTCP(true); // optionalI implemented it with method overloading on sendAndReceiveTcp and sendAndReceiveUDP. In this way it does not affect the tests.