簡介
上一篇文章,我們講到了netty對SOCKS訊息提供了SocksMessage物件的封裝,並且區分SOCKS4和SOCKS5,同時提供了連線和響應的各種狀態。
有了SOCKS訊息的封裝之後,我們還需要做些什麼工作才能搭建一個SOCKS伺服器呢?
使用SSH搭建SOCKS伺服器
其實最簡單的辦法就是使用SSH工具來建立SOCKS代理伺服器。
先看下SSH建立SOCKS服務的命令:
ssh -f -C -N -D bindaddress:port name@server
-f 表示SSH作為守護程式進入後臺執行。
-N 表示不執行遠端命令,只用於埠轉發。
-D 表示是埠上的動態轉發。這個命令支援SOCKS4和SOCKS5。
-C 表示傳送前壓縮資料。
bindaddress 本地伺服器的繫結地址。
port 表示本地伺服器的指定偵聽埠。
name 表示ssh伺服器登入名。
server表示ssh伺服器地址。
上面命令的意思是,在本機建立埠繫結,然後將其轉發到遠端的代理伺服器上。
比如我們可以在本機開一個2000的埠,將其轉發到遠端168.121.100.23這臺機子上:
ssh -f -N -D 0.0.0.0:2000 root@168.121.100.23
有了代理伺服器之後,就可以使用了,首先介紹一個怎麼在curl命令中使用SOCKS代理。
我們想通過代理伺服器,訪問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伺服器
使用netty搭建SOCKS伺服器的關鍵是使用netty伺服器做中繼,它需要建立兩個連線,一個是客戶端到代理伺服器的連線,一個是代理伺服器到目標地址的連線。接下來,我們一步一步探討如何在netty中構建SOCKS伺服器。
搭建伺服器的基本步驟和普通的伺服器基本一致,要注意的就是對訊息的編碼、解碼和在訊息讀取處理過程中的轉發。
encoder和decoder
對於一種協議來說,最終要的就是對應的encoder和decoder,用於協議物件和ByteBuf之間進行轉換。
netty提供的SOCKS轉換器叫做SocksPortUnificationServerHandler。先看下它的定義:
public class SocksPortUnificationServerHandler extends ByteToMessageDecoder
它繼承自ByteToMessageDecoder表示是ByteBuf和Socks物件之間的轉換。
所以我們在ChannelInitializer中只需要加上SocksPortUnificationServerHandler和自定義的處Socks訊息的handler即可:
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new LoggingHandler(LogLevel.DEBUG),
new SocksPortUnificationServerHandler(),
SocksServerHandler.INSTANCE);
}
等等,不對呀!有細心的小夥伴可能發現了,SocksPortUnificationServerHandler只是一個decoder,我們還缺少一個encoder,用來將Socks物件轉換成本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方法裡面,根據Socks的版本不同,給ctx新增了對應的encoder和decoder,非常的巧妙。
對應的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會在後面進行詳細講解,這裡大家知道它使用來建立連線即可。
對於Socks5來說,就比較複雜點,包含了初始化請求、認證請求和建立連線三個部分,所以需要分別處理:
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();
}
注意,這裡我們的認證請求只支援使用者名稱密碼認證。
ConnectHandler
既然是作為一個代理伺服器,就需要建立兩個連線,一個是客戶端到代理伺服器的連線,一個是代理伺服器到目標伺服器的連線。
對於netty來說,這兩個連線可以用兩個Bootstrap來建立。
其中客戶端到代理伺服器端的連線我們在啟動netty伺服器的時候已經建立了,所以需要在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());
}
});
新的Bootstrap需要從接收到的Socks訊息中取出目標伺服器的地址和埠,然後建立連線。
然後判斷新建立連線的狀態,如果成功則新增一個轉發器將outboundChannel的訊息轉發到inboundChannel中,同時將inboundChannel的訊息轉發到outboundChannel中,從而達到伺服器代理的目的。
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());
}
總結
說白了,代理伺服器就是建立兩個連線,將其中一個連線的訊息轉發給另外一個連線。這種操作在netty中是非常簡便的。
本文的例子可以參考:learn-netty4
本文已收錄於 http://www.flydean.com/37-netty-cust-socks-server/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!