trojan-gfw / trojan

An unidentifiable mechanism that helps you bypass GFW.
https://trojan-gfw.github.io/trojan/
GNU General Public License v3.0
18.96k stars 3.04k forks source link

How to build a trojan protocol #711

Open mohuangNPC opened 2 weeks ago

mohuangNPC commented 2 weeks ago

I know this may not be within the scope of your answer, but I still hope you can help me out. I want to use Java to construct a Trojan request, and it does follow the data in the document, but it never works. This is my code. I'm sorry, I don't know where the problem is.

package org.example;

import org.example.entity.TrojanWrapperRequest;
import org.example.util.Sha224Util;
import javax.net.ssl.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * @author mohuangNPC
 * @version 1.0
 * @date 2024/11/4 9:19
 */
public class TrojanMainTest {
    private static final int LISTEN_PORT = 7890;
    private static final String TROJAN_HOST = "xxx.xxx.xxx";
    private static final int TROJAN_PORT = 443;
    private static final String password = "password1";

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(LISTEN_PORT)) {
            System.out.println("Trojan proxy server listening on port " + LISTEN_PORT);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                new Thread(new ClientHandler(clientSocket)).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * The specific thread that accepts the request
     * +-----------------------+---------+----------------+---------+----------+
     * | hex(SHA224(password)) |  CRLF   | Trojan Request |  CRLF   | Payload  |
     * +-----------------------+---------+----------------+---------+----------+
     * |          56           | X'0D0A' |    Variable    | X'0D0A' | Variable |
     * +-----------------------+---------+----------------+---------+----------+
     *
     * where Trojan Request is a SOCKS5-like request:
     *
     * +-----+------+----------+----------+
     * | CMD | ATYP | DST.ADDR | DST.PORT |
     * +-----+------+----------+----------+
     * |  1  |  1   | Variable |    2     |
     * +-----+------+----------+----------+
     */
    static class ClientHandler implements Runnable {
        private final Socket clientSocket;

        public ClientHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        @Override
        public void run() {
            long threadId = Thread.currentThread().getId();
            String requestLine = "";
            try (InputStream clientInput = clientSocket.getInputStream();
                 OutputStream clientOutput = clientSocket.getOutputStream()) {
                TrojanWrapperRequest trojanWrapperRequest = new TrojanWrapperRequest();
                TrojanWrapperRequest.TrojanRequest trojanRequest = new TrojanWrapperRequest.TrojanRequest();
                // Set password and Request
                {
                    trojanRequest.setCmd(0X01);
                    String dstAddr = "";
                    int dstPort = 443;
                    int atyp = 1;
                    {
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        int byteRead;
                        while ((byteRead = clientInput.read()) != -1) {
                            if (byteRead == '\n') {
                                break;
                            }
                            baos.write(byteRead);
                        }
                        requestLine = baos.toString(StandardCharsets.UTF_8.name());
                        System.err.println(threadId+"----Received line: " + requestLine);
                        if (requestLine != null) {
                            String[] parts = requestLine.split(" ");
                            if (parts.length > 1) {
                                String url = parts[1];
                                String host = extractHostDomain(url);
                                int port = extractPort(url);
                                System.err.println(threadId+"----Host: " + host);
                                dstAddr = host;
                                System.err.println(threadId+"----Port: " + port);
                                dstPort = port;
                                System.err.println(threadId+"----Is IP: " + isValidIPAddress(host));
                                if (isValidIPAddress(host)) {
                                    atyp = 1;
                                } else {
                                    atyp = 3;
                                }
                            }
                        }
                    }
                    trojanRequest.setAtyp(atyp);
                    trojanRequest.setDstPort(dstPort);
                    trojanRequest.setDstAddr(dstAddr);
                }
                trojanWrapperRequest.setPassword(Sha224Util.encryptThisString(password));
                trojanWrapperRequest.setTrojanRequest(trojanRequest);
                // Do not verify certificates
                {
                    TrustManager[] trustAllCerts = new TrustManager[]{
                            new X509TrustManager() {
                                @Override
                                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                                    return null;
                                }

                                @Override
                                public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
                                }

                                @Override
                                public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
                                }
                            }
                    };
                    SSLContext sc = SSLContext.getInstance("SSL");
                    sc.init(null, trustAllCerts, new java.security.SecureRandom());
                    SSLSocketFactory factory = sc.getSocketFactory();
                    try (SSLSocket trojanSocket = (SSLSocket) factory.createSocket(TROJAN_HOST, TROJAN_PORT);
                         InputStream trojanInput = trojanSocket.getInputStream();
                         OutputStream trojanOutput = trojanSocket.getOutputStream()) {
                        // begin handshake
                        trojanSocket.startHandshake();
                        System.err.println(threadId+"----trojan connect success");
                        ByteArrayOutputStream out = new ByteArrayOutputStream();
                        // Assemble the data according to the Trojan protocol
                        {
                            out.write(trojanWrapperRequest.getPassword().getBytes(StandardCharsets.UTF_8));
                            out.write(0X0D);
                            out.write(0X0A);
                            out.write(trojanRequest.getCmd());
                            out.write(trojanRequest.getAtyp());
                            encodeAddress(trojanRequest.getAtyp(), out, trojanRequest.getDstAddr());
                            out.write((trojanRequest.getDstPort() >> 8) & 0xFF);
                            out.write(trojanRequest.getDstPort() & 0xFF);
                            out.write(0X0D);
                            out.write(0X0A);
                            // The domain name port has been read once when obtaining it, so it needs to be added here
                            out.write(requestLine.getBytes(StandardCharsets.UTF_8));
                        }
                        byte[] byteArray = out.toByteArray();
                        trojanOutput.write(byteArray);
                        Thread thread = new Thread(() -> {
                            clientToProxy(threadId, clientInput, trojanOutput);
                        });
                        thread.start();
                        clientToProxy(threadId,clientInput, trojanOutput);
                        proxyToClient(threadId,trojanInput, clientOutput);
                        // Waiting for the client to proxy forwarding to complete
                        try {
                            thread.join();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    clientSocket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Data from the client to the server
     * @param threadId
     * @param input
     * @param output
     */
    private static void clientToProxy(long threadId,InputStream input, OutputStream output) {
        byte[] buffer = new byte[4096];
        int bytesRead;
        try {
            while ((bytesRead = input.read(buffer)) != -1) {
                output.write(buffer, 0, bytesRead);
                output.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Response from the proxy server
     * @param threadId
     * @param input
     * @param output
     */
    private static void proxyToClient(long threadId,InputStream input, OutputStream output) {
        byte[] buffer = new byte[4096];
        int bytesRead;
        try {
            while ((bytesRead = input.read(buffer)) != -1) {
                output.write(buffer, 0, bytesRead);
                output.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String extractHost(String url) {
        if (url.startsWith("http://")) {
            url = url.substring(7);
        } else if (url.startsWith("https://")) {
            url = url.substring(8);
        }
        String s = url.split("/")[0];
        return url.split("/")[0];
    }

    /**
     * Get the domain name or IP
     * @param url
     * @return
     */
    private static String extractHostDomain(String url) {
        if (url.startsWith("http://")) {
            url = url.substring(7);
        } else if (url.startsWith("https://")) {
            url = url.substring(8);
        }
        String s = url.split("/")[0];
        return s.split(":")[0];
    }

    /**
     * Get Port
     * @param url
     * @return
     */
    private static int extractPort(String url) {
        int defaultPort = url.startsWith("https://") ? 443 : 80;
        String host = extractHost(url);
        if (host.contains(":")) {
            String[] parts = host.split(":");
            return Integer.parseInt(parts[1]);
        }
        return defaultPort;
    }

    /**
     * Determine whether it is an IP or a domain name
     * @param ip
     * @return
     */
    private static boolean isValidIPAddress(String ip) {
        String ipPattern =
                "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
                        "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
                        "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
                        "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
        return ip.matches(ipPattern);
    }

    /**
     * Processing IP or domain name
     * @param addressType
     * @param out
     * @param dstAddr
     * @throws IOException
     */
    private static void encodeAddress(int addressType, ByteArrayOutputStream out, String dstAddr) throws IOException {
        if (addressType == 1) {
            String[] split = dstAddr.split("\\.");
            for (String item : split) {
                int b = Integer.parseInt(item);
                out.write(b);
            }
        } else if (addressType == 3) {
            out.write(dstAddr.length());
            out.write(dstAddr.getBytes(StandardCharsets.UTF_8));
        } else {
            throw new RuntimeException("error address");
        }
    }
}
Babybatrick commented 2 weeks ago

Check SHA-224 Hash Implementation to ensure that your Sha224Util.encryptThisString(password) function is generating the SHA-224 hash as expected, Trojan expects the hex-encoded version of the SHA-224 hash, so make sure the result is correct.

Trojan protocol expects specific line breaks (CRLF, 0x0D0A), so check that the formatting exactly matches the protocol, like the following if i am not mistaking (but dont take my word for it): After the password hash, you should have a CRLF (0x0D, 0x0A). At the end of the Trojan request section, you should also include a CRLF.

When encoding destination addresses, for domain names (ATYP 0x03), make sure you correctly write the length byte, and the UTF-8 address after. And ye, make sure the encoding is right.

Socks5-like request structure - make sure the trojan request follows the exact byte order CMD (0x01) – usually represents CONNECT. ATYP (0x01 for IPv4 or 0x03 for domain names). DST.ADDR – encoded according to ATYP. DST.PORT – encoded in big-endian (network byte order). (found this on the internet, maybe it helps)

As i understand you are spawning a thread to forward data from clientInput to trojanOutput, and also handling data from trojanInput to clientOutput in proxyToClient(). just make sure these threads dont interfere with each other, maybe by calling flush() after writing data to ensure it's sent immediately

Also check that the SSLContext is correctly initialized and that trojanSocket.startHandshake() successfully establishes an ssl connection

Also, the simplest, check network configs and firewall rules.