package ganarchy.friendcode.client; import ganarchy.friendcode.FriendCode; import ganarchy.friendcode.sam.I2PSamControl; import ganarchy.friendcode.sam.I2PSamStreamConnector; import java.io.IOException; import java.net.Inet6Address; import java.net.ServerSocket; import java.net.Socket; public class SamProxyThread extends Thread { private final String target; private volatile int localPort; private volatile Status status = Status.IDLE; private volatile boolean running = true; private volatile boolean isConnecting = false; public SamProxyThread(String target) { this.target = target; this.setDaemon(true); this.setName("SAM Proxy Control Thread"); } @Override public void run() { if (this.running) { tryRun(true); } if (this.running) { tryRun(false); } this.status = Status.SETUP_FAILED; } private void tryRun(final boolean zeroHop) { // this is all very inefficient but we don't particularly care // who cares if you have 3 threads just to connect to a minecraft server/friend code // it's only 3 threads // the "server" is far more efficient, only requiring one try (final I2PSamControl control = new I2PSamControl(zeroHop, null)) { if (!control.connect()) { this.running = false; this.status = Status.CONNECTION_FAILED; return; } if (!control.start()) { this.running = false; this.status = Status.SETUP_FAILED; return; } switch (control.checkName(this.target)) { case UNKNOWN, INVALID -> { this.running = false; this.status = Status.RESOLUTION_FAILED; return; } case FAILED -> { return; } } // we can't distinguish if we should retry the connection // (e.g. because building tunnels took too long, destination is known but gateway/peer is not, etc) // vs if we should try with 1hop (because we can't reach peer from our connection, // e.g. ipv4-only client vs ipv6-only friend, symmetric NAT, etc) // so we just try to "warm up" the connection and hope it works. I2PSamStreamConnector warmup = control.connectStream(this.target); if (!warmup.connect()) { this.running = false; this.status = Status.CONNECTION_FAILED; return; } warmup.start(); warmup.close(); I2PSamStreamConnector stream = control.connectStream(this.target); if (!stream.connect()) { this.running = false; this.status = Status.CONNECTION_FAILED; return; } this.isConnecting = true; if (!stream.start()) { FriendCode.LOGGER.warn("[SAM error] {}", stream.getStatus()); this.isConnecting = false; if (!zeroHop) { this.running = false; this.status = Status.SETUP_FAILED; } return; } try ( Socket socket = stream.unwrap(); ServerSocket server = new ServerSocket(0, 8, Inet6Address.getByAddress("localhost", new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 0)); ) { this.localPort = server.getLocalPort(); server.setSoTimeout(3000); this.status = Status.RUNNING; try (Socket single = server.accept()) { socket.setTcpNoDelay(true); single.setTcpNoDelay(true); Thread clientThread = new Thread("SAM Proxy CTS Thread") { @Override public void run() { try ( var clientRead = single.getInputStream(); var serverWrite = socket.getOutputStream(); ) { byte[] buffer = new byte[128*1024]; while (SamProxyThread.this.running) { int read = clientRead.read(buffer); if (read > 0) { serverWrite.write(buffer, 0, read); } } } catch (IOException ignored) { } } }; Thread serverThread = new Thread("SAM Proxy STC Thread") { @Override public void run() { try ( var serverRead = socket.getInputStream(); var clientWrite = single.getOutputStream(); ) { byte[] buffer = new byte[128*1024]; while (SamProxyThread.this.running) { int read = serverRead.read(buffer); if (read > 0) { clientWrite.write(buffer, 0, read); } } } catch (IOException ignored) { } } }; clientThread.setDaemon(true); clientThread.start(); serverThread.setDaemon(true); serverThread.start(); while (!this.isInterrupted() && this.running) { if (control.sendPing() < 0) { this.running = false; } try { Thread.sleep(1500L); } catch (InterruptedException ignored) { } } } finally { this.running = false; } } } catch (IOException e) { // don't really need to do anything } } public void stopProxy() { this.running = false; // FIXME this is wrong, need to actually close sockets. this.interrupt(); } public Status status() { return this.status; } public int port() { return this.localPort; } public boolean isConnecting() { return this.isConnecting; } public enum Status { IDLE, RUNNING, CONNECTION_FAILED, SETUP_FAILED, RESOLUTION_FAILED; } }