基于Java NIO实现SOCKS代理协议

SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。
当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。

SOCKS协议解析

SOCKS代理需要以下几个步骤

1 握手,客户端与代理协商SOCKS5版本及账号密码

2 请求建立连接,客户端将发送要请求的远程地址发送给代理

3 发送数据包给代理服务,代理进行转发后,将目标服务的数据返回

sock5

根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间。

OSI网络模型

OSI_model

Shadowsocks工作原理

Shadowsocks是基于Python开发的科学上网软件。Shadowsocks实现了SOCKS协议,SOCKS协议本身是没有加密功能的,为了防止GFW根据报文对数据进行拦截,所以要对数据进行加密。
Shadowsocks分为 SS LocalSS Server

  • SS Local负责通过SOCKS协议与客户端交互,将客户端(浏览器)请求的数据加密发送给SS Server和将SS Server响应的数据解密返回给客户端。
  • SS Server负责将SS Local请求的数据解密,发送给目标网站(例如google.com),并将目标网站的响应进行加密返回。

Shadowsocks

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));
}

参考文档

wiki

Shadowsocks

Shadowsocks 源码分析——协议与结构


基于Java NIO实现SOCKS代理协议
https://blog.yjll.blog/post/e99ab4dc.html
作者
简斋
发布于
2020年5月15日
许可协议