netty trojan

参考代码:https://github.com/kdyzm/trojan-client-netty
参考博客:
github代码作者的博客:https://blog.kdyzm.cn/post/71
trojan-go介绍:https://p4gefau1t.github.io/trojan-go/developer/trojan/
trojan协议介绍:https://trojan-gfw.github.io/trojan/protocol
socks5协议详解(中文博客):https://www.ddhigh.com/2019/08/24/socks5-protocol/

使用

配置文件

netty trojan_第1张图片

查看是否生效

netty trojan_第2张图片

研究

责任链

在哪将数据发送到远程?

channelRead:48, TrojanClient2DestInboundHandler (com.kdyzm.trojan.client.netty.inbound)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
channelRead:1410, DefaultChannelPipeline$HeadContext (io.netty.channel)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
access$600:61, AbstractChannelHandlerContext (io.netty.channel)
run:370, AbstractChannelHandlerContext$7 (io.netty.channel)
safeExecute:164, AbstractEventExecutor (io.netty.util.concurrent)
runAllTasks:472, SingleThreadEventExecutor (io.netty.util.concurrent)
run:500, NioEventLoop (io.netty.channel.nio)
run:989, SingleThreadEventExecutor$4 (io.netty.util.concurrent)
run:74, ThreadExecutorMap$2 (io.netty.util.internal)
run:30, FastThreadLocalRunnable (io.netty.util.concurrent)
run:842, Thread (java.lang)

发送流程

4-1线程 添加监听
com.kdyzm.trojan.client.netty.inbound.http.HttpProxyInboundHandler#proxyConnect
责任链:HttpServerCodec->HttpProxyInboundHandler
责任链初始化:com.kdyzm.trojan.client.netty.server.NettyServerInitializer#initChannel
主要目的:
1、创建客户端
2、向客户端添加监听(会在2-1线程中执行监听内容)。客户端创建完成后将服务端的责任链修改为TrojanClient2DestInboundHandler

2-1线程 添加任务 io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead(io.netty.channel.AbstractChannelHandlerContext, java.lang.Object)
责任链:无(当责任链所属线程不是当前运行的线程时,会抛给所属线程)
主要目的:向服务端发送任务

4-1线程 添加任务
com.kdyzm.trojan.client.netty.inbound.TrojanClient2DestInboundHandler#channelRead
责任链:TrojanClient2DestInboundHandler
责任链初始化:com.kdyzm.trojan.client.netty.inbound.http.HttpProxyInboundHandler#proxyConnect
netty trojan_第3张图片
主要目的:调用客户端发送数据
netty trojan_第4张图片

接收流程

责任链:SslHandler->TrojanDest2ClientInboundHandler
责任链初始化:com.kdyzm.trojan.client.netty.inbound.http.HttpProxyInboundHandler#proxyConnect
netty trojan_第5张图片

主要目的:将客户端接收到的结果通过服务端返回给调用者

我觉得原始代码逻辑复杂,不利于学习,所以按照自己的思维习惯把代码改造了一下。
https://github.com/shangjianan2/trojan-client-netty/tree/wjl
wjl这个分支

和原有代码相比最大的不同有两点。
1、服务端不会改变责任链,一直都会是HttpServerCodec->HttpProxyInboundHandlerV3->TrojanClient2DestInboundHandlerV3。
原始代码中,当客户端创建完时服务端会改变其责任链。
netty trojan_第6张图片

2、服务端和客户端在NettyServerInitializerV3上完成了通道的交换。原始代码中这步是在com.kdyzm.trojan.client.netty.inbound.http.HttpProxyInboundHandler#proxyConnect完成的。之所以把"通道交换"放在NettyServerInitializerV3是因为我觉得这属于初始化的工作,放到初始化相关类中逻辑可能会更清晰一些。

通道交换指的是客户端要有服务端的channel,服务端也要有客户端的channel。这样服务端才能把浏览器的数据传到客户端,客户端才能把远程返回的数据返回给服务端进而展示到浏览器

发送协议

结合trojan协议规则分析代码实现。以下都是我自己的感悟,不一定正确(协议方面理解的不正确的地方欢迎指正),仅供参考。

为了便于研究trojan协议运行方式,我根据github上的代码写了一段测试代码。这个测试代码中会每秒通过远程服务对某个网站发送请求。

package com.kdyzm.trojan.client.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class TestMain {

    /**
     * trojan远程服务的密码
     */
    private static final String _password = "用自己的";
    /**
     * trojan远程服务地址
     */
    private static final String _host = "用自己的";



    public static final int IPV4 = 0X01;

    public static final int DOMAIN = 0X03;

    public static final int IPV6 = 0x04;

    /**
     * trojan远程服务端口
     */
    private static int _port = 443;

    /**
     * 用于构建第一个请求包。注意:DST.ADDR这个字段需要填写域名,不能带有协议。
     */
    private static final String _uri = "www.google.com";
    /**
     * 构建http协议内容。注意:必须带有协议
     */
    private static final String _uri_http = "http://www.google.com/";
    private static final int _uri_port = 80;

    public static void main(String[] args) throws Throwable {
        /**
         * 构建客户端。与远程建立TLS连接
         */
        EventLoopGroup clientWorkGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        ChannelFuture future = bootstrap.group(clientWorkGroup)
                .channel(NioSocketChannel.class)
                .remoteAddress(_host, _port)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(SslContextBuilder.forClient()
                                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                .build()
                                .newHandler(ch.alloc()));// 处理TSL连接
                        ch.pipeline().addLast(new HttpResponseDecoder());// 节码
                        ch.pipeline().addLast("http-aggregator",
                                new HttpObjectAggregator(65536));// 粘包拆包。注意:若不使用此handler,下一个handler的入参将不会是FullHttpResponse的子类,将无法打印content
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                if (msg instanceof FullHttpResponse) {
                                    FullHttpResponse httpResponse = (FullHttpResponse) msg;
                                    System.out.println("status:" + httpResponse.status().code());
                                    System.out.println(httpResponse.headers());
                                    System.out.println(httpResponse.content().toString(StandardCharsets.UTF_8));
                                    System.out.println("===============\r\n\r\n\r\n\r\n======================");
                                }
                                super.channelRead(ctx, msg);
                            }
                        });
                    }
                })
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .connect();
        future.get();// 同步等待客户端建立完成

        Channel channel = future.channel();


        /**
         * 发送第一个包,建立Trojan连接
         */
        DefaultHttpHeaders headers = new DefaultHttpHeaders();
        headers.set("User-Agent", "curl/8.4.0");// 这些header都是按照"curl -x 127.0.0.1:10810 www.google.com"这个指令编写的,具体含义暂时不讨论
        headers.set("Accept", "*/*");
        headers.set("Proxy-Connection", "Keep-Alive");
        headers.set("Host", "www.google.com");
        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, _uri_http, headers);// 创建访问www.google.com的http请求

        EmbeddedChannel em = new EmbeddedChannel(new HttpRequestEncoder());
        em.writeOutbound(req);// 将http请求转换成ByteBuf
        final ByteBuf payload = (ByteBuf) em.readOutbound();
        em.close();

        ByteBuf out = Unpooled.buffer(1024);// 开始构建第一个trojan请求。第一个trojan请求中需要携带很多信息,之后的请求中不需要。
        String password = encryptThisString(_password);// hex(SHA224(password))
        out.writeCharSequence(password, StandardCharsets.UTF_8);
        out.writeByte(0X0D);// CRLF
        out.writeByte(0X0A);
        out.writeByte(1);// CMD
        out.writeByte(DOMAIN);// ATYP
        encodeAddress(DOMAIN, out, _uri);// DST.ADDR
        out.writeShort(_uri_port);// DST.PORT
        out.writeByte(0X0D);// CRLF
        out.writeByte(0X0A);
        out.writeBytes(payload.copy());// 这里必须用copy,否则使用一次之后payload就不能用了。因为rdx已经右移到wdx,没有可读内容。

        channel.writeAndFlush(out);

        System.out.println("init over:" + channel.isActive());

        /**
         * 直接将http包发送到远程
         */
        while (true) {
            Thread.sleep(1000);
            channel.writeAndFlush(payload.copy());
        }
    }

    private static void encodeAddress(int addressType, ByteBuf out, String dstAddr) {
        if (addressType == IPV4) {
            String[] split = dstAddr.split("\\.");
            for (String item : split) {
                int b = Integer.parseInt(item);
                out.writeByte(b);
            }
        } else if (addressType == DOMAIN) {
            out.writeByte(dstAddr.length());
            out.writeCharSequence(dstAddr, StandardCharsets.UTF_8);
        } else {
            //TODO 暂时不支持ipV6
            throw new RuntimeException("无法支持的地址类型");
        }
    }

    public static String encryptThisString(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-224");
            byte[] messageDigest = md.digest(input.getBytes());
            BigInteger no = new BigInteger(1, messageDigest);
            StringBuilder hashtext = new StringBuilder(no.toString(16));
            while (hashtext.length() < 32) {
                hashtext.insert(0, "0");
            }
            return hashtext.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}

trojan基于TLS,先完成TLS连接的建立,然后再此基础上建立Trojan的连接。
Trojan连接的建立过程很简单。TLS建立之后,向远程发送的第一个数据按照Trojan的报文格式包装,当第一个包装之后的数据通过远程服务器的校验之后,后续所有此通道上的请求都可以直接发到远程,不需要再次包装。

编写测试代码过程中遇到的问题:
1、Trojan协议中DST.ADDR这个字段仅填写域名,不能填写协议。

2、处理response时,若责任链中不添加HttpObjectAggregator,下一个责任链节点中接收到的数据将不会是FullHttpResponse的子类,将无法打印content

3、第一个trojan包中为啥一定要带有访问的域名?我理解建立trojan连接时只需要密码校验就可以了,应该不需要通过域名进行什么校验吧?
协议介绍中有以下这段话
If the request is valid, the trojan server connects to the endpoint indicated by the DST.ADDR and DST.PORT field and opens a direct tunnel between the endpoint and trojan client.
DST.ADDR和DST.PORT不是为了校验,而是为了指定trojan client与哪个endpoint建立tunnel。简单理解就是DST.ADDR和DST.PORT决定了此tunnel可以处理哪些请求。若后续包中没携带DST.ADDR和DST.PORT且http报文中域名与第一个包中的DST.ADDR和DST.PORT不一致,消息将发送失败。
netty trojan_第7张图片

netty trojan_第8张图片

待解决疑问:
1、是否可以更换协议?若在第二或者第三个包中指定了DST.ADDR和DST.PORT,是否可以将此通道建立的tunnel转移到另一个endpoint上?

你可能感兴趣的:(java,netty,trojan)