基于Java NIO实现SOCKS代理协议
SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。
当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。
SOCKS协议解析
SOCKS代理需要以下几个步骤
1 握手,客户端与代理协商SOCKS5版本及账号密码
2 请求建立连接,客户端将发送要请求的远程地址发送给代理
3 发送数据包给代理服务,代理进行转发后,将目标服务的数据返回
根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间。
OSI网络模型
Shadowsocks工作原理
Shadowsocks是基于Python开发的科学上网软件。Shadowsocks实现了SOCKS协议,SOCKS协议本身是没有加密功能的,为了防止GFW根据报文对数据进行拦截,所以要对数据进行加密。
Shadowsocks分为 SS Local
和 SS Server
。
SS Local
负责通过SOCKS协议与客户端交互,将客户端(浏览器)请求的数据加密发送给SS Server
和将SS Server
响应的数据解密返回给客户端。SS Server
负责将SS Local
请求的数据解密,发送给目标网站(例如google.com),并将目标网站的响应进行加密返回。
NIO实现
首先我们要先创建一个选择器和一个ServerSocketChannel,ServerSocketChannel订阅ACCEPT事件注册到选择器上,selector.select()
会阻塞直到有请求到来时。
public void execute() throws IOException {
log.info("starting server at port {}", ssConfig.getLocalPort());
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(ssConfig.getLocalPort()));
// 非阻塞
serverSocketChannel.configureBlocking(false);
// 订阅事件,当有请求时激活
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞线程,直到有订阅的事件
int count = selector.select();
if (count <= 0) continue;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (!selectionKey.isValid()) continue;
// 一个新的连接进来时,
if (selectionKey.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
// 新进来的SocketChannel
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
// 我们希望从中获取数据,我们注册读事件,当数据可读时会通知我们
// 并绑定一个处理器,当数据可读时,使用这个处理器进行数据处理
socketChannel.register(selector, SelectionKey.OP_READ, new ChannelHandler(ssConfig, selector, socketChannel));
} else {
// 交由对应的处理器来处理
ChannelHandler channelHandler = (ChannelHandler) selectionKey.attachment();
if (Objects.nonNull(channelHandler)) {
channelHandler.handle(selectionKey);
}
}
}
}
}
处理器将根据事件状态进行分发
public void handle(SelectionKey key) {
SocketChannel channel = (SocketChannel) key.channel();
try {
// 与浏览器交互的Channel
if (localSocketChannel == channel) {
// 当有request时,读取数据
if (key.isReadable()) {
onLocalRead(channel);
}
// 与远程服务交互的Channel
} else if (remoteSocketChannel == channel) {
// 远程可连接时,发送加密数据包
if (key.isConnectable()) {
onRemoteConnect(key, channel);
}
// 当远程返回数据时,进行解密,将解密数据返回给浏览器
if (key.isReadable()) {
onRemoteRead(channel);
}
}
} catch (Exception e) {
log.error(e.toString(), e);
this.stage = SocksStage.STAGE_DESTROYED;
}
// 销毁
if (this.stage == SocksStage.STAGE_DESTROYED) {
destroy();
}
}
与浏览器交互的Channel可读取时表示有新的数据到来,需从Channel读取数据,并写入响应数据
private void onLocalRead(SocketChannel socketChannel) throws IOException {
SocksStage socksStage = this.stage;
int read = socketChannel.read(byteBuffer);
byte[] readData = ByteBuffers.convertByte(byteBuffer);
if (readData.length == 0) {
this.stage = SocksStage.STAGE_DESTROYED;
return;
}
// 通过状态控制socks5交互的步骤
switch (socksStage) {
/*
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1~255 |
+----+----------+----------+
*/
// 握手,告知支持的协议
case STAGE_INIT:
socketChannel.write(ByteBuffer.wrap(new byte[]{0x05, 0x00}));
this.stage = SocksStage.STAGE_ADDR;
break;
/*
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | 1 | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
*/
// request中有要访问的域名,此处需要与远程建立连接,订阅OP_CONNECT事件,当可连接时通知到我们。
case STAGE_ADDR:
remoteSocketChannel = SocketChannel.open();
remoteSocketChannel.configureBlocking(false);
remoteSocketChannel.register(this.selector, SelectionKey.OP_CONNECT, this);
remoteSocketChannel.connect(new InetSocketAddress(ssConfig.getServer(), ssConfig.getServerPort()));
socketChannel.write(ByteBuffer.wrap(new byte[]{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10}));
this.stage = SocksStage.STAGE_CONNECTING;
// 0x05 0x01 0x00 0x03 0x0a b'google.com' 0x00 0x50
// 0x0a 表示域名长度
Socks5Utils.parseAddr(readData);
// 去掉无用数据,缓存向服务端请求的数据
this.byteQueue.add(Arrays.copyOfRange(readData, 3, read));
break;
case STAGE_CONNECTING:
// 远程未连接,但是客户端数据已经到达,将数据缓存到队列中
this.byteQueue.add(readData);
break;
case STAGE_STREAM:
// 将数据加密发送给远程服务器
byte[] encrypt = this.cryptHelper.encrypt(readData);
this.remoteSocketChannel.write(ByteBuffer.wrap(encrypt));
break;
}
}
与浏览器交互的第2步时,会与远程建立连接,订阅连接事件。此处为可连接时的处理,将数据推送给远程,并订阅读取事件准备接受远程服务器的响应
private void onRemoteConnect(SelectionKey key, SocketChannel socketChannel) throws IOException {
if (socketChannel.finishConnect()) {
key.interestOps(SelectionKey.OP_READ);
this.stage = SocksStage.STAGE_STREAM;
for (byte[] bytes : byteQueue) {
byte[] encrypt = this.cryptHelper.encrypt(bytes);
socketChannel.write(ByteBuffer.wrap(encrypt));
}
}
}
远程服务可读时表示有数据返回,读取并将数据解密,返回给浏览器
private void onRemoteRead(SocketChannel socketChannel) throws IOException {
socketChannel.read(byteBuffer);
byte[] readData = ByteBuffers.convertByte(byteBuffer);
if (readData.length == 0) {
this.stage = SocksStage.STAGE_DESTROYED;
return;
}
byte[] decrypt = this.cryptHelper.decrypt(readData);
this.localSocketChannel.write(ByteBuffer.wrap(decrypt));
}
参考文档
基于Java NIO实现SOCKS代理协议
https://blog.yjll.blog/post/e99ab4dc.html