netty系列之:從零到壹,搭建一個SOCKS代理服務(wù)器

簡介

上一篇文章,我們講到了netty對SOCKS消息提供了SocksMessage對象的封裝,并且區(qū)分SOCKS4和SOCKS5,同時提供了連接和響應(yīng)的各種狀態(tài)。

有了SOCKS消息的封裝之后,我們還需要做些什么工作才能搭建一個SOCKS服務(wù)器呢?

使用SSH搭建SOCKS服務(wù)器

其實(shí)最簡單的辦法就是使用SSH工具來建立SOCKS代理服務(wù)器。

先看下SSH建立SOCKS服務(wù)的命令:

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

-f 表示SSH作為守護(hù)進(jìn)程進(jìn)入后臺執(zhí)行。

-N 表示不執(zhí)行遠(yuǎn)程命令,只用于端口轉(zhuǎn)發(fā)。

-D 表示是端口上的動態(tài)轉(zhuǎn)發(fā)。這個命令支持SOCKS4和SOCKS5。

-C 表示發(fā)送前壓縮數(shù)據(jù)。

bindaddress 本地服務(wù)器的綁定地址。

port 表示本地服務(wù)器的指定偵聽端口。

name 表示ssh服務(wù)器登錄名。

server表示ssh服務(wù)器地址。

上面命令的意思是,在本機(jī)建立端口綁定,然后將其轉(zhuǎn)發(fā)到遠(yuǎn)程的代理服務(wù)器上。

比如我們可以在本機(jī)開一個2000的端口,將其轉(zhuǎn)發(fā)到遠(yuǎn)程168.121.100.23這臺機(jī)子上:

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

有了代理服務(wù)器之后,就可以使用了,首先介紹一個怎么在curl命令中使用SOCKS代理。

我們想通過代理服務(wù)器,訪問www.flydean.com,該怎么做呢?

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

要想檢測SOCKS的連接,還可以使用netcat命令如下:

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

使用netty搭建SOCKS服務(wù)器

使用netty搭建SOCKS服務(wù)器的關(guān)鍵是使用netty服務(wù)器做中繼,它需要建立兩個連接,一個是客戶端到代理服務(wù)器的連接,一個是代理服務(wù)器到目標(biāo)地址的連接。接下來,我們一步一步探討如何在netty中構(gòu)建SOCKS服務(wù)器。

搭建服務(wù)器的基本步驟和普通的服務(wù)器基本一致,要注意的就是對消息的編碼、解碼和在消息讀取處理過程中的轉(zhuǎn)發(fā)。

encoder和decoder

對于一種協(xié)議來說,最終要的就是對應(yīng)的encoder和decoder,用于協(xié)議對象和ByteBuf之間進(jìn)行轉(zhuǎn)換。

netty提供的SOCKS轉(zhuǎn)換器叫做SocksPortUnificationServerHandler。先看下它的定義:

public class SocksPortUnificationServerHandler extends ByteToMessageDecoder

它繼承自ByteToMessageDecoder表示是ByteBuf和Socks對象之間的轉(zhuǎn)換。

所以我們在ChannelInitializer中只需要加上SocksPortUnificationServerHandler和自定義的處Socks消息的handler即可:

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

等等,不對呀!有細(xì)心的小伙伴可能發(fā)現(xiàn)了,SocksPortUnificationServerHandler只是一個decoder,我們還缺少一個encoder,用來將Socks對象轉(zhuǎn)換成本ByteBuf,這個encoder在哪里呢?

別急,我們再回到SocksPortUnificationServerHandler中,在它的decode方法中,有這樣一段代碼:

 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;

原來是在decode方法里面,根據(jù)Socks的版本不同,給ctx添加了對應(yīng)的encoder和decoder,非常的巧妙。

對應(yīng)的encoder分別是Socks4ServerEncoder和Socks5ServerEncoder。

建立連接

對于Socks4來說,只有一個建立連接的請求類型,在netty中用Socks4CommandRequest來表示。

所以我們只需要在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();
                }

這里我們添加了一個自定義的SocksServerConnectHandler,用來處理Socks連接,這個自定義handler會在后面進(jìn)行詳細(xì)講解,這里大家知道它使用來建立連接即可。

對于Socks5來說,就比較復(fù)雜點(diǎn),包含了初始化請求、認(rèn)證請求和建立連接三個部分,所以需要分別處理:

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

注意,這里我們的認(rèn)證請求只支持用戶名密碼認(rèn)證。

ConnectHandler

既然是作為一個代理服務(wù)器,就需要建立兩個連接,一個是客戶端到代理服務(wù)器的連接,一個是代理服務(wù)器到目標(biāo)服務(wù)器的連接。

對于netty來說,這兩個連接可以用兩個Bootstrap來建立。

其中客戶端到代理服務(wù)器端的連接我們在啟動netty服務(wù)器的時候已經(jīng)建立了,所以需要在ConnectHandler中,建立一個新的代理服務(wù)器到目標(biāo)服務(wù)器的連接:

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 {
                    // 關(guān)閉連接
                    ctx.channel().writeAndFlush(
                            new DefaultSocks4CommandResponse(Socks4CommandStatus.REJECTED_OR_FAILED)
                    );
                    closeOnFlush(ctx.channel());
                }
            });

新的Bootstrap需要從接收到的Socks消息中取出目標(biāo)服務(wù)器的地址和端口,然后建立連接。

然后判斷新建立連接的狀態(tài),如果成功則添加一個轉(zhuǎn)發(fā)器將outboundChannel的消息轉(zhuǎn)發(fā)到inboundChannel中,同時將inboundChannel的消息轉(zhuǎn)發(fā)到outboundChannel中,從而達(dá)到服務(wù)器代理的目的。

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

總結(jié)

說白了,代理服務(wù)器就是建立兩個連接,將其中一個連接的消息轉(zhuǎn)發(fā)給另外一個連接。這種操作在netty中是非常簡便的。

本文的例子可以參考:learn-netty4

本文已收錄于 http://www.flydean.com/37-netty-cust-socks-server/

最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發(fā)現(xiàn)!

歡迎關(guān)注我的公眾號:「程序那些事」,懂技術(shù),更懂你!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容