Introduction

In the previous article, we mentioned that netty provides encapsulation of SocksMessage objects for SOCKS messages, distinguishes SOCKS4 and SOCKS5, and provides various states of connection and response.

With the encapsulation of SOCKS messages, what else do we need to do to build a SOCKS server?

Use SSH to build a SOCKS server

In fact, the easiest way is to use the SSH tool to establish a SOCKS proxy server.

First look at the SSH command to establish a SOCKS service:

ssh -f -C -N -D bindaddress:port name@server

-f means SSH enters the background as a daemon.

-N means do not execute remote commands, only for port forwarding.

-D means dynamic forwarding on the port. This command supports SOCKS4 and SOCKS5.

-C means to compress data before sending.

bindaddress The bind address of the local server.

port represents the designated listening port of the local server.

name represents the login name of the ssh server.

server represents the address of the ssh server.

The above command means to establish port binding on the local machine, and then forward it to the remote proxy server.

For example, we can open a port 2000 on the local machine and forward it to the remote machine 168.121.100.23:

ssh -f -N -D 0.0.0.0:2000 root@168.121.100.23

Once you have a proxy server, you can use it. First, I will introduce how to use a SOCKS proxy in the curl command.

We want to access www.flydean.com through a proxy server, what should we do?

curl -x socks5h://localhost:2000 -v -k -X GET http://www.flydean.com:80

To detect the SOCKS connection, you can also use the netcat command as follows:

ncat –proxy 127.0.0.1:2000 –proxy-type socks5 www.flydean.com 80 -nv

Use netty to build a SOCKS server

The key to using netty to build a SOCKS server is to use the netty server as a relay. It needs to establish two connections, one is the connection from the client to the proxy server, and the other is the connection from the proxy server to the target address. Next, we step by step to discuss how to build a SOCKS server in netty.

The basic steps of setting up a server are basically the same as those of a normal server. What should be paid attention to is the encoding and decoding of messages and the forwarding in the process of message reading and processing.

encoder and decoder

For a protocol, the final thing is the corresponding encoder and decoder, used to convert between the protocol object and ByteBuf.

The SOCKS converter provided by netty is called SocksPortUnificationServerHandler. First look at its definition:

public class SocksPortUnificationServerHandler extends ByteToMessageDecoder

It inherits from ByteToMessageDecoder and means that it is a conversion between ByteBuf and Socks objects.

So we only need to add SocksPortUnificationServerHandler and a custom Socks message handler to ChannelInitializer:

    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(
                new LoggingHandler(LogLevel.DEBUG),
                new SocksPortUnificationServerHandler(),
                SocksServerHandler.INSTANCE);
    }

Wait, no! Some careful friends may have discovered that SocksPortUnificationServerHandler is just a decoder. We still lack an encoder to convert Socks objects into ByteBuf. Where is this encoder?

Don't worry, let's go back to SocksPortUnificationServerHandler. In its decode method, there is such a piece of code:

 case SOCKS4a:
            logKnownVersion(ctx, version);
            p.addAfter(ctx.name(), null, Socks4ServerEncoder.INSTANCE);
            p.addAfter(ctx.name(), null, new Socks4ServerDecoder());
            break;
        case SOCKS5:
            logKnownVersion(ctx, version);
            p.addAfter(ctx.name(), null, socks5encoder);
            p.addAfter(ctx.name(), null, new Socks5InitialRequestDecoder());
            break;

It turned out to be in the decode method. Depending on the version of Socks, the corresponding encoder and decoder were added to ctx, which is very clever.

The corresponding encoders are Socks4ServerEncoder and Socks5ServerEncoder.

establish connection

For Socks4, there is only one request type to establish a connection, which is represented by Socks4CommandRequest in netty.

So we only need to determine the requested version in channelRead0:

case SOCKS4a:
                Socks4CommandRequest socksV4CmdRequest = (Socks4CommandRequest) socksRequest;
                if (socksV4CmdRequest.type() == Socks4CommandType.CONNECT) {
                    ctx.pipeline().addLast(new SocksServerConnectHandler());
                    ctx.pipeline().remove(this);
                    ctx.fireChannelRead(socksRequest);
                } else {
                    ctx.close();
                }

Here we have added a custom SocksServerConnectHandler to handle the Socks connection. This custom handler will be explained in detail later, and everyone knows that it can be used to establish a connection.

For Socks5, it is more complicated, including three parts: initialization request, authentication request and connection establishment, so they need to be processed separately:

case SOCKS5:
                if (socksRequest instanceof Socks5InitialRequest) {
                    ctx.pipeline().addFirst(new Socks5CommandRequestDecoder());
                    ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.NO_AUTH));
                } else if (socksRequest instanceof Socks5PasswordAuthRequest) {
                    ctx.pipeline().addFirst(new Socks5CommandRequestDecoder());
                    ctx.write(new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.SUCCESS));
                } else if (socksRequest instanceof Socks5CommandRequest) {
                    Socks5CommandRequest socks5CmdRequest = (Socks5CommandRequest) socksRequest;
                    if (socks5CmdRequest.type() == Socks5CommandType.CONNECT) {
                        ctx.pipeline().addLast(new SocksServerConnectHandler());
                        ctx.pipeline().remove(this);
                        ctx.fireChannelRead(socksRequest);
                    } else {
                        ctx.close();
                    }

Note that our authentication request here only supports username and password authentication.

ConnectHandler

Since it is used as a proxy server, two connections need to be established, one is the connection from the client to the proxy server, and the other is the connection from the proxy server to the target server.

For netty, these two connections can be established with two Bootstraps.

The connection from the client to the proxy server is already established when we start the netty server, so we need to establish a new proxy server to the target server connection in ConnectHandler:

private final Bootstrap b = new Bootstrap();

Channel inboundChannel = ctx.channel();
            b.group(inboundChannel.eventLoop())
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ClientPromiseHandler(promise));

            b.connect(request.dstAddr(), request.dstPort()).addListener(future -> {
                if (future.isSuccess()) {
                    // 成功建立连接
                } else {
                    // 关闭连接
                    ctx.channel().writeAndFlush(
                            new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED)
                    );
                    closeOnFlush(ctx.channel());
                }
            });

The new Bootstrap needs to retrieve the address and port of the target server from the received Socks message, and then establish a connection.

Then determine the status of the newly established connection, if successful, add a forwarder to forward the message of the outboundChannel to the inboundChannel, and at the same time forward the message of the inboundChannel to the outboundChannel, so as to achieve the purpose of server proxy.

 final Channel outboundChannel = future.getNow();
                        if (future.isSuccess()) {
                            ChannelFuture responseFuture = ctx.channel().writeAndFlush(
                                    new DefaultSocks4CommandResponse(Socks4CommandStatus.SUCCESS));
                            //成功建立连接,删除SocksServerConnectHandler,添加RelayHandler
                            responseFuture.addListener(channelFuture -> {
                                ctx.pipeline().remove(SocksServerConnectHandler.this);
                                outboundChannel.pipeline().addLast(new RelayHandler(ctx.channel()));
                                ctx.pipeline().addLast(new RelayHandler(outboundChannel));
                            });
                        } else {
                            ctx.channel().writeAndFlush(
                                    new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED));
                            closeOnFlush(ctx.channel());
                        }

Summarize

To put it bluntly, the proxy server is to establish two connections and forward messages from one connection to the other. This operation is very simple in netty.

For examples in this article, please refer to: learn-netty4

This article has been included in http://www.flydean.com/37-netty-cust-socks-server/

The most popular interpretation, the most profound dry goods, the most concise tutorial, and many tips you don't know are waiting for you to discover!

Welcome to pay attention to my official account: "programs, those things", know the technology, know you better!


flydean
890 声望437 粉丝

欢迎访问我的个人网站:www.flydean.com