参考代码: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/
在哪将数据发送到远程?
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
主要目的:调用客户端发送数据
责任链:SslHandler->TrojanDest2ClientInboundHandler
责任链初始化:com.kdyzm.trojan.client.netty.inbound.http.HttpProxyInboundHandler#proxyConnect
主要目的:将客户端接收到的结果通过服务端返回给调用者
我觉得原始代码逻辑复杂,不利于学习,所以按照自己的思维习惯把代码改造了一下。
https://github.com/shangjianan2/trojan-client-netty/tree/wjl
wjl这个分支
和原有代码相比最大的不同有两点。
1、服务端不会改变责任链,一直都会是HttpServerCodec->HttpProxyInboundHandlerV3->TrojanClient2DestInboundHandlerV3。
原始代码中,当客户端创建完时服务端会改变其责任链。
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不一致,消息将发送失败。
待解决疑问:
1、是否可以更换协议?若在第二或者第三个包中指定了DST.ADDR和DST.PORT,是否可以将此通道建立的tunnel转移到另一个endpoint上?