使用netty实现trojan协议客户端
一直想使用java实现trojan客户端,接触到netty之后感觉使用netty这个高性能的网络框架来实现非常合适。下面说一说实现过程中遇到的问题和解决方式。
现在主要的trojan客户端应该是v2ray,它是用C++写的,除此之外,使用java编写的客户端还没见到过。。所以一开始遇到的问题还是蛮大的,比如trojan协议看不懂的问题。
一、trojan协议
trojan的github地址:https://github.com/trojan-gfw/trojan
trojan官网:https://trojan-gfw.github.io/trojan/
trojan协议说明地址:https://trojan-gfw.github.io/trojan/protocol
根据trojan协议文档上的说明,实现trojan协议必须满足以下要求:
- 服务端必须是https,与客户端的通信必须使用tls加密,否则拒绝连接
- tls握手成功后,客户端应当发送以下格式的数据到服务端
+-----------------------+---------+----------------+---------+----------+ | 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 | +-----+------+----------+----------+ where: o CMD o CONNECT X'01' o UDP ASSOCIATE X'03' o ATYP address type of following address o IP V4 address: X'01' o DOMAINNAME: X'03' o IP V6 address: X'04' o DST.ADDR desired destination address o DST.PORT desired destination port in network octet order
- 它是一个类socks5请求的格式,socks5详情可以参考 https://tools.ietf.org/html/rfc1928
说实话,看完文档我是懵逼的,总觉得哪里不对又说不出来。最终我有了以下几个疑问
- tls连接之后要发送以下结构的内容
hex(SHA224(password)) | CRLF | Trojan Request | CRLF | Payload |
---|---|---|---|---|
56 | X'0D0A' | Variable | X'0D0A' | Variable |
我认为这是trojan协议握手的请求,这个时候Payload字段有何作用? 难道说同一个连接中的每次数据请求都要将请求的数据放到Payload字段吗。。。 文档中表示发送完该内容后trojan服务端会打开一个连接目标服务器的通道,然后转发该连接中的所有请求到目标服务器,关键是这第一个请求没有响应客户端吗,socks5的connet请求是要响应客户端的吧,这里trojan协议握手是不是也有个响应才对,响应格式是啥
- Trojan Request 字段
CMD | ATYP | DST.ADDR | DST.PORT |
---|---|---|---|
1 | 1 | Variable | 2 |
协议中 DST.ADDR是可变长字段,这个字段如果是Sockts5-like,是不是应该如果是ipv4,则固定4个字节长度;如果是域名类型,则第一个字节是域名长度,后面是域名内容?
我将这两个疑问在github上提了个issue,链接如下:https://github.com/trojan-gfw/trojan/issues/585 。根据相关开发描述,Payload字段是第一次请求的时候带上的真正的请求信息,响应内容就是目标服务器所响应的内容(言外之意就是无socks5那样的connection响应);至于DST.ADDR的规则则和socks5协议一样。
二、源码印证
为了验证对trojan协议的猜想,不得不翻看了些trojan服务端的源代码,项目地址:https://github.com/trojan-gfw/trojan
1.trojan客户端发起请求服务端基本校验
这段代码上来就对请求体进行了有效性验证,如果请求有效才对密码进行校验;如果请求体无效,则服务端会打印日志not trojan request, connecting to...
所以说,校验逻辑是啥?校验的代码在这里:https://github.com/trojan-gfw/trojan/blob/master/src/proto/trojanrequest.cpp#L23
这段代码做了如下事情:
- 客户端请求必须带有\r\n字符,否则判定无效
- 密码字段从第一个字符到\r\n所在的位置
- 跳过\r\n字符,剩下的内容中,取出来第一个字符作为CMD字段,紧接着对ATYP+DST.ADDR 地址字段进行了校验
- 如果地址无效或者剩余内容中无\r\n,则请求无效
- payload字段本来是trojan request+\r\n+payload,更改为真正的payload。
2.地址校验逻辑
校验的代码逻辑在这里:https://github.com/trojan-gfw/trojan/blob/master/src/proto/socks5address.cpp#L25
bool SOCKS5Address::parse(const string &data, size_t &address_len) {
if (data.length() == 0 || (data[0] != IPv4 && data[0] != DOMAINNAME && data[0] != IPv6)) {
return false;
}
address_type = static_cast<AddressType>(data[0]);
switch (address_type) {
case IPv4: {
if (data.length() > 4 + 2) {
address = to_string(uint8_t(data[1])) + '.' +
to_string(uint8_t(data[2])) + '.' +
to_string(uint8_t(data[3])) + '.' +
to_string(uint8_t(data[4]));
port = (uint8_t(data[5]) << 8) | uint8_t(data[6]);
address_len = 1 + 4 + 2;
return true;
}
break;
}
case DOMAINNAME: {
uint8_t domain_len = data[1];
if (domain_len == 0) {
// invalid domain len
break;
}
if (data.length() > (unsigned int)(1 + domain_len + 2)) {
address = data.substr(2, domain_len);
port = (uint8_t(data[domain_len + 2]) << 8) | uint8_t(data[domain_len + 3]);
address_len = 1 + 1 + domain_len + 2;
return true;
}
break;
}
case IPv6: {
if (data.length() > 16 + 2) {
char t[40];
sprintf(t, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
uint8_t(data[1]), uint8_t(data[2]), uint8_t(data[3]), uint8_t(data[4]),
uint8_t(data[5]), uint8_t(data[6]), uint8_t(data[7]), uint8_t(data[8]),
uint8_t(data[9]), uint8_t(data[10]), uint8_t(data[11]), uint8_t(data[12]),
uint8_t(data[13]), uint8_t(data[14]), uint8_t(data[15]), uint8_t(data[16]));
address = t;
port = (uint8_t(data[17]) << 8) | uint8_t(data[18]);
address_len = 1 + 16 + 2;
return true;
}
break;
}
}
return false;
}
从这段代码可以看出,如果是ipv4地址类型,直接取四个字节作为ip地址;如果是域名类型,则取第一个字节作为域名长度,接下来的域名长度那么长的字节数作为域名内容;如果是IPv6类型,则取16个字节作为地址长度。无论是哪种地址类型,接下来都取两个字节作为端口号。
3.对payload的处理
https://github.com/trojan-gfw/trojan/blob/master/src/session/serversession.cpp#L169
if (valid) {
out_write_buf = req.payload;
if (req.command == TrojanRequest::UDP_ASSOCIATE) {
Log::log_with_endpoint(in_endpoint, "requested UDP associate to " + req.address.address + ':' + to_string(req.address.port), Log::INFO);
status = UDP_FORWARD;
udp_data_buf = out_write_buf;
udp_sent();
return;
} else {
Log::log_with_endpoint(in_endpoint, "requested connection to " + req.address.address + ':' + to_string(req.address.port), Log::INFO);
}
} else {
Log::log_with_endpoint(in_endpoint, "not trojan request, connecting to " + query_addr + ':' + query_port, Log::WARN);
out_write_buf = data;
}
sent_len += out_write_buf.length();
...省略部分代码...
out_socket.async_connect(*iterator, [this, self, query_addr, query_port](const boost::system::error_code error) {
if (error) {
Log::log_with_endpoint(in_endpoint, "cannot establish connection to remote server " + query_addr + ':' + query_port + ": " + error.message(), Log::ERROR);
destroy();
return;
}
Log::log_with_endpoint(in_endpoint, "tunnel established");
status = FORWARD;
out_async_read();
if (!out_write_buf.empty()) {
out_async_write(out_write_buf);
} else {
in_async_read();
}
});
上述代码中,在请求有效的情况下,首先将payload赋值给了out_write_buf,然后连接目标服务器,将out_write_buf转发给目标服务器。当然这部分代码是在status == HANDSHAKE
的情况下,做完这些事情之后,会更改status的状态为FORWARD,再次客户端再次发过来的数据,会被全部转发到目标服务器,不再走“握手”的过程。
三、netty实现trojan客户端
实现trojan客户端要基于原来的socks5服务端实现,socks5接收客户端请求,然后使用trojan客户端转发到trojan服务端,由trojan服务端转发请求到真正的目标服务器。
原来的socks5服务端代码:https://github.com/kdyzm/trojan-client-netty/releases/tag/v3.0
设计上,还是要在socks5协议的第三阶段实现trojan客户端,只需要将真正的目标服务器替换成trojan服务端地址,然后按照协议请求格式请求即可。但是和原来的稍稍有所不同,因为socks5协议的connnet请求是要先响应客户端,客户端拿到响应之后才真正的发送请求数据;而trojan协议则是第一次请求直接将认证信息和请求的数据放到一起发送到trojan服务端,服务端验证没问题之后直接发送请求到目标服务器,不再单独为connet请求单独响应客户端,所以客户端要为每个请求单独维持一个状态。
处理器 | 作用 |
---|---|
com.kdyzm.trojan.client.netty.inbound.TrojanDest2ClientInboundHandler | 转发trojan服务端的响应数据到客户端,其逻辑和Dest2ClientInboundHandler逻辑相同 |
com.kdyzm.trojan.client.netty.inbound.TrojanClient2DestInboundHandler | 转发客户端的请求到trojan服务端,因为第一次的请求不一样,所以维护了一个状态 |
com.kdyzm.trojan.client.netty.encoder.TrojanRequestEncoder | 基于TrojanClient2DestInboundHandler,将对象转化为trojan协议所需的格式 |
四、源代码和参考文档
源代码:https://github.com/kdyzm/trojan-client-netty
参考文档:
https://trojan-gfw.github.io/trojan/protocol
https://github.com/trojan-gfw/trojan/issues/585
五、遇到的问题
换行符\r\n对应着协议中的X'0D0A',不能少,要分开写0X0D和0X0A;该程序要和Proxifier配合使用,如果出现了连接速度缓慢,有些网页打不开的情况,是因为Proxifier没设置好,一定要注意使用代理的dns设置,菜单:Profile->Name Resolution 取消Detect DNS settings automatically
选项,勾选Resolve hostnames through proxy
,之后就好了。
注意:本文归作者所有,未经作者允许,不得转载