netty series: building a SOCKS proxy server from zero to one

Posted by nrerup on Wed, 05 Jan 2022 02:34:05 +0100

brief introduction

In the previous article, we mentioned that netty provides the encapsulation of SocksMessage object for SOCKS messages, distinguishes between 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?

Using SSH to build SOCKS server

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

Let's first look at the command of SSH to establish SOCKS service:

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

-f indicates that SSH is executed in the background as a daemon.

-N means that the remote command is not executed and is only used for port forwarding.

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

-C means compressed data before sending.

bindaddress the binding address of the local server.

Port represents the specified listening port of the local server.

Name indicates the ssh server login name.

Server indicates the ssh server address.

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

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

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

After you have a proxy server, you can use it. First, introduce how to use SOCKS proxy in curl command.

We want to visit www.flybean.com through a proxy server What should I do? Www. 68mn?

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

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

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

Using netty to build SOCKS server

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

The basic steps of building a server are basically the same as those of an ordinary server. Attention should be paid to the encoding, decoding and forwarding of messages during message reading and processing.

encoder and decoder

For a protocol, the final is the corresponding encoder and decoder, which are used for the conversion between the protocol object and ByteBuf.

The SOCKS converter provided by netty is called socksport unification server handler. Let's look at its definition first:

public class SocksPortUnificationServerHandler extends ByteToMessageDecoder

It inherits from ByteToMessageDecoder and represents the conversion between ByteBuf and Socks objects.

Therefore, in ChannelInitializer, we only need to add socksportunification serverhandler and custom handler for handling Socks messages:

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

Wait, no! A careful partner may find that the socksportunification serverhandler is just a decoder. We still lack an encoder to convert Socks objects to ByteBuf. Where is this encoder?

Don't worry. Let's go back to socksportunification serverhandler. In its decode method, there is such a 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 turns out that in the decode method, corresponding encoder s and decoder s are added to ctx according to different versions of Socks, which is very clever.

The corresponding encoders are Socks4ServerEncoder and Socks5ServerEncoder respectively.

Establish connection

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

Therefore, we only need to judge 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 Socks connections. This custom handler will be explained in detail later. You know that it can be used to establish connections.

For Socks5, it is more complex, including initialization request, authentication request and connection establishment, so it needs 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 user name and password authentication.

ConnectHandler

Since you are acting as a proxy server, you need 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 server.

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

The connection from the client to the proxy server has been established when we start the netty server, so we need to establish a new connection from the proxy server to the target server 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()) {
                    // The connection was established successfully
                } else {
                    // Close connection
                    ctx.channel().writeAndFlush(
                            new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED)
                    );
                    closeOnFlush(ctx.channel());
                }
            });

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

Then judge the status of the newly established connection. If it is successful, add a forwarder to forward the messages from the outboundChannel to the inboundChannel, and forward the messages from the inboundChannel to the outboundChannel, so as to achieve the purpose of server agent.

 final Channel outboundChannel = future.getNow();
                        if (future.isSuccess()) {
                            ChannelFuture responseFuture = ctx.channel().writeAndFlush(
                                    new DefaultSocks4CommandResponse(Socks4CommandStatus.SUCCESS));
                            //The connection is successfully established, the SocksServerConnectHandler is deleted, and the RelayHandler is added
                            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());
                        }

summary

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

Examples of this article can be referred 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 find!

Welcome to my official account: "those things in procedure", understand technology, know you better!

Topics: Java Netty socks5