工具介绍
项目地址:
https://github.com/ffay/lanproxy
工具简介
lanproxy是一个将局域网个人电脑、服务器代理到公网的内网穿透工具,支持tcp流量转发,可支持任何tcp上层协议。
功能特点
- 穿透基础功能,同开源版本,高性能,可同时支持数万穿透连接
- 全新界面UI,操作简单,部署简单(java+mysql)
- 自定义域名绑定,为你穿透端口绑定域名,不再是IP+端口裸奔访问
- 自定义域名ssl证书,也可为你绑定的域名开启ssl证书自动申请与续期,无需人工干涉
- 自定义客户端离线展示页面,可以利用该功能展示一些html单页
- 支持http/https/socks5多种模式使用客户端网络代理上网,家里轻松访问公司网络
- 多用户支持,同时满足多人日常穿透需求
流量&逻辑分析
环境&测试准备
环境配置
内部服务器Ubantu 192.168.2.129 Lanproxy客户端
中转服务器 Ubantu https://lanp.nioee.com/ 提供的官方地址(或者自己搭建也可以)
测试流程
- 按照Github上的提示,先生成好对应的客户端和服务端,然后配置好Ubantu相关配置
- 然后运行start.sh
- 此时上官网发现相关连接已经建立
- 找一台外网vps测试ssh连接
检测特征和流量分析
一般情况下存在两种连接方式,一种是带SSL证书的连接和数据传输,默认对应端口为4993;另一种是不带的SSL证书的连接方式,默认端口为4900。
客户端逻辑分析
下载好相关项目之后,通过看start.bat能发现入口函数为org.fengfei.lanproxy.client.ProxyClientContaine
简要跟踪了一下相关的函数和功能,主要功能入口函数在connectProxyServer
private void connectProxyServer() {
//实际上调用了doConnect用于建立和对应服务器的三次握手连接
bootstrap.connect(config.getStringValue("server.host"), config.getIntValue("server.port")).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
// 连接成功,向服务器发送客户端认证信息(clientKey)
// 设置通信信道
ClientChannelMannager.setCmdChannel(future.channel());
// 生成ProxyMessage对象,对象实例内容如下
// ProxyMessage [type=0, serialNumber=0, uri=null, data=null]
ProxyMessage proxyMessage = new ProxyMessage();
// 设置类型为C_TYPE_AUTH(1)
proxyMessage.setType(ProxyMessage.C_TYPE_AUTH);
// 设置uri为认证的key
proxyMessage.setUri(config.getStringValue("client.key"));
//用于发送数据,调用writeAndFlush方法,就是Netty编码流
future.channel().writeAndFlush(proxyMessage);
sleepTimeMill = 1000;
logger.info("connect proxy server success, {}", future.channel());
} else {
logger.warn("connect proxy server failed", future.cause());
// 连接失败,发起重连
reconnectWait();
connectProxyServer();
}
}
});
}
关于Netty的writeAndFlush可参考链接:https://juejin.cn/post/7010924544378535950
关于定义的encode方法如下:
其中头部长度为10,数据长度取决于uri的长度也就是配置文件中client.key的长度,以及data的长度。初始的第一个链接请求,其data为null,之后写入顺序和逻辑如下:
out.writeInt(bodyLength) // 4字节,表示所有数据的长度 这里为00 00 00 2a(key默认生成为32位)
out.writeByte(msg.getType()) // 1字节,这里为01
out.writeLong(msg.getSerialNumber()) // 8字节,这里为00 00 00 00 00 00 00 00
out.writeByte((byte) uriBytes.length); //1字节,这里为20(32位长度)
out.writeBytes(uriBytes) //这里为32位数长度,具体看情况,key可自定义
客户端主要接受3种类型数据
处理逻辑存在差异,但是思想差不多,cmd通道和数据通道,cmd通过用于命令传输,数据通道用于实际数据通讯,数据通道的类型必定为先03后05,格式满足ProxyMessage的类型
服务端逻辑分析
详细的数据处理流程和Cleint端基本完全一致,具体的处理逻辑在ServerChannelHandler中,针对不同的请求类型做不同的处理逻辑:
针对初始请求的C_TYPE_AUTH类型的处理逻辑如下:
private void handleAuthMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
//获取client发送过来数据中的client.key
String clientKey = proxyMessage.getUri();
//获取代理客户端对应的代理服务器端口
List<Integer> ports = ProxyConfig.getInstance().getClientInetPorts(clientKey);
//如果没找到对应clientkey的对应配置,则关闭连接
if (ports == null) {
logger.info("error clientKey {}, {}", clientKey, ctx.channel());
ctx.channel().close();
return;
}
//检查是否存在相关的命令通信通道
Channel channel = ProxyChannelManager.getCmdChannel(clientKey);
if (channel != null) {
logger.warn("exist channel for key {}, {}", clientKey, channel);
ctx.channel().close();
return;
}
logger.info("set port => channel, {}, {}, {}", clientKey, ports, ctx.channel());
//加入命令通信信道中
ProxyChannelManager.addCmdChannel(ports, clientKey, ctx.channel());
}
服务端在这一步中不会发送相关数据,后面可能会发送一些心跳包之类的数据,数据格式固定为
00 00 00 0a 07 00 00 00 00 00 00 00 00
关于其他ProxyMessage的不同请求的处理逻辑,主要分析TYPE_CONNECT,其他都类似:
ProxyMessage.TYPE_CONNECT
服务端的发送逻辑处理在UserChannelHandler中实现,服务端在接收到来自其他的连接,连接client和serrver的对应端口时,最终会调用channelActive进行一系列处理,包括初始化connect类型的proxyMessage,之后会调用writeAndFlush进行发送,通过层层调用最终会到ProxyMessageEncoder的encode方法进行发送处理
encode方法在client中做过相关的说明和跟踪,这里不继续详细分析,直接给相关的结果
out.writeInt(bodyLength) // 4字节,表示所有数据的长度,这里长度不固定主要和lan设置有关
out.writeByte(msg.getType()) // 1字节,这里为03
out.writeLong(msg.getSerialNumber()) // 8字节,这里为00 00 00 00 00 00 00 00
out.writeByte((byte) uriBytes.length); //1字节,这里为AtomicLong(0) addAndGet()的值,不确定长度和内容
out.writeBytes(uriBytes) //具体看情况,长度内容不固定
out.writeBytes(msg.getData()) //ip:port形式,具体看生成的客户端配置,比如我的是127.0.0.1:22
注:serialNumber在通过查找上下文引用和初始化中,都未发现有被特殊设置的痕迹,由client端生成,默认都是00 00 00 00 00 00 00 00,server端设置为和client逻辑相同的id
配置客户端的后端IP端口只允许Ip:PORT形式,不然client端调用InetSocketAddress会失败
TLS特征
如果配置文件中启用了TLS,那么会使用conf目录下的jks文件来进行证书校验和验证,如果使用自定义证书检测存在困难,如果使用默认的test,jks可以做部分检测,使用keytool来查看默认的jks内容如下:
keytool -list -v -keystore test.jks #密码为123456
密钥库类型: JKS
密钥库提供方: SUN
您的密钥库包含 1 个条目
别名: test
创建日期: 2017年5月9日
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=f, OU=ts, O=ts, L=bj, ST=bj, C=cn
发布者: CN=f, OU=ts, O=ts, L=bj, ST=bj, C=cn
序列号: 18b6ca05
生效时间: Tue May 09 18:16:01 CST 2017, 失效时间: Wed May 09 18:16:01 CST 2018
证书指纹:
SHA1: B9:87:38:E0:51:4C:B7:5D:F0:9B:6F:E0:19:74:6D:5A:FD:92:FD:88
SHA256: B6:39:F9:ED:7C:58:C8:5A:23:59:79:BD:D8:7D:CC:ED:D4:48:17:49:1D:21:31:3D:28:6B:28:89:30:DE:4A:04
签名算法名称: SHA256withRSA
主体公共密钥算法: 1024 位 RSA 密钥 (弱)
版本: 3
扩展:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 9F 3F EF FD 18 9E 14 E9 B6 DC 97 7C CE 30 03 70 .?...........0.p
0010: 1D AF 06 65 ...e
]
]
*******************************************
*******************************************
其中由于TLS1.3的传输特性,其中关于证书的部分都不能直接以明文的形式看到,只能针对TLS1.1和TLS1.2做检测,主要是TLS1.2(1.1基本已被废弃)
直接检测证书的相关字段即可