用Netty实现Ngrok Client【原创】

作者: w_j_y | 来源:发表于2019-06-29 17:35 被阅读31次

    什么是Ngrok


    有时候我们需要临时将本地运行的web项目发布到公网,但没有公网ip,或者需要在家访问公司内网上的某台电脑的某个端口。这个时候就需要借助Ngrok来实现上述目的,Ngrok是一个内网穿透工具。

    如何使用Ngrok


    一套完整的Ngrok包含两个部分:Ngrok Server和Ngrok Client

    Ngrok Server 需要部署在有公网ip的服务器上,Ngrok Client则可以部署在任意能够访问外网的电脑上。

    当客户端启动且与服务端连接交换信息后,服务端会分配一个端口给客户端(例如52228),与客户端建立一条新的tcp连接,此后,通过访问服务端的52228端口,就相当于访问客户端中配置的需要被发布到公网的端口。

    有人可能会问,“我本来就没有公网ip,如何部署Ngrok Server?”,对此,可以去Ngrok官网注册用户,使用Ngrok官网提供的Ngrok Server。

    由于本片主要讲解Ngrok Client的Netty实现,具体部署过程不做详解。

    Ngrok网络协议


    Ngrok官方客户端采用C编写,也有网友提供了Python的实现,以下网络协议通过分析Python版的源码得到。
    Ngrok网络协议的数据交换过程如下图所示:

    image

    上图不包含client和server之间的心跳包数据(client端口1和server端口1之间通过心跳维持连接)

    各个端口含义:

    server端口1 : server启动时配置的监听端口,默认是4443

    client端口1 : client与server端口1建立连接时的端口,由操作系统分配。

    server端口2 : client 发送ReqTunnel请求中携带的要求server暴露的端口,若client不指定端口,则是server随机分配的一个端口。

    client端口2: client与server建立的另一个用来转发代理数据的端口,由操作系统分配。

    client端口3:client与本地服务建立连接时的端口,由操作系统分配。

    协议的具体数据内容:

    Auth:

    { "Type": "Auth", "Payload": { "ClientId": "", "OS": "darwin", "Arch": "amd64", "Version": "2", "MmVersion": "1.7", "User": "user", "Password": "" }}

    AuthResp:

    {"Type":"AuthResp","Payload":{"Version":"2","ClientId":"d720a2bcb084f5669d7ef7af7fd8ad9c","Error":"","MmVersion":"1.7"}}

    ReqTunnel:

    {"Type": "ReqTunnel", "Payload": {"ReqId": "jhnl8GF3", "Protocol": "tcp", "Hostname": "", "Subdomain": "www", "HttpAuth": "", "RemotePort": 55499}}

    ReqProxy:

    {"Type":"ReqProxy","Payload":{}}

    RegProxy:

    {"Type": "RegProxy", "Payload": {"ClientId": "d720a2bcb084f5669d7ef7af7fd8ad9c"}}

    NewTunnel:

    {"Type":"NewTunnel","Payload":{"Error":"","ReqId":"jhnl8GF3","Protocol":"tcp","Url":"tcp://codewjy.top:55499"}}

    Ping:

    {"Type":"Ping","Payload":{}}

    Pong:

    {"Type":"Pong","Payload":{}}

    通过Netty实现


    了解了ngrok的网络协议,下面通过netty实现这一协议

    按照协议的先后顺序,一步一步实现,

    首先是client与server的控制连接的建立(上图中client端口1和server端口1的连接),同时也是客户端的启动入口NgrokClient:

    /**
     * HOST: ngrok服务端域名
     * PORT: ngrok服务端控制端口
     * REMORTE_PORT: ngrok服务端代理端口
     * LOCAL_PORT: 本地需要被暴露出来的端口
     */
        static final String HOST = "codewjy.top";
        static final int PORT = 4454;
        static final int REMORTE_PORT = 55499;
        static final int LOCAL_PORT = 8080;
    
        public static void main(String[] args) {
            new NgrokClient().start();
        }
    
        private void start() {
            NioEventLoopGroup group = new NioEventLoopGroup(1);
            Bootstrap b = new Bootstrap();
            try {
                b.group(group)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            protected void initChannel(SocketChannel ch) throws SSLException {
                                SSLEngine engine = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build().newEngine(ch.alloc());
                                ChannelPipeline p = ch.pipeline();
                                //ssl处理器
                                p.addFirst(new SslHandler(engine,false));
                                //以下两个处理器组成心跳处理器
                                p.addLast(new IdleStateHandler(5, 20, 0, TimeUnit.SECONDS));
                                p.addLast(new HeartBeatHandler());
                                //主控制处理器
                                p.addLast(new ControlHandler());
                            }
                        });
                ChannelFuture f = b.connect(NgrokClient.HOST, NgrokClient.PORT).sync();
                f.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    
    

    ControlHandler部分代码

        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            //channel激活的时候发送Auth
            ctx.channel().writeAndFlush(GenericUtil.getByteBuf(AUTH));
        }
    
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
            if (byteBuf.isReadable()) {
                int rb = byteBuf.readableBytes();
                if (rb > 8) {
                    CharSequence charSequence = byteBuf.readCharSequence(rb, Charset.defaultCharset());
                    JSONObject jsonObject = JSON.parseObject(charSequence.toString());
                    if ("AuthResp".equals(jsonObject.get("Type"))) {
                        //收到AuthResp响应
                        clientId = jsonObject.getJSONObject("Payload").getString("ClientId");
                        ctx.channel().writeAndFlush(GenericUtil.getByteBuf(PING));
                        //发送ReqTunnel
                        ctx.channel().writeAndFlush(GenericUtil.getByteBuf(REQ_TUNNEL));
                    }else if ("ReqProxy".equals(jsonObject.get("Type"))) {
                        //收到ReqProxy响应
                        Bootstrap b = new Bootstrap();
                        try {
                            b.group(group)
                                    .channel(NioSocketChannel.class)
                                    .option(ChannelOption.TCP_NODELAY, true)
                                    .handler(new ChannelInitializer<SocketChannel>() {
                                        protected void initChannel(SocketChannel ch) throws SSLException {
                                            SSLEngine engine = SslContextBuilder.forClient()
                                                    .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                                    .build()
                                                    .newEngine(ch.alloc());
                                            ChannelPipeline p = ch.pipeline();
                                            //ssl处理器
                                            p.addFirst(new SslHandler(engine,false));
                                            //代理处理器
                                            p.addLast(new ProxyHandler(clientId));
                                        }
                                    });
                            ChannelFuture f = b.connect(NgrokClient.HOST, NgrokClient.PORT).sync();
                            logger.info("connect to remote address "+f.channel().remoteAddress());
                            f.channel().closeFuture().addListener((ChannelFutureListener) channelFuture -> logger.info("disconnect to remote address "+f.channel().remoteAddress()));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else if ("NewTunnel".equals(jsonObject.get("Type"))) {
                        logger.info(jsonObject.toJSONString());
                    }
                }
            }
    
        }
    
    

    以上代码完成了client和server的握手
    下面处理server发起开始代理部分的协议,也就是ProxyHandler的内容
    ProxyHandler部分代码

        //持有连接到本地服务的channel,用于将数据转发给本地服务
        private ChannelFuture f;
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            //channel激活后,发送RegProxy
            ctx.channel().writeAndFlush(GenericUtil.getByteBuf(REG_PROXY));
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws InterruptedException {
            ByteBuf byteBuf = (ByteBuf) msg;
            if (byteBuf.isReadable()) {
                int rb = byteBuf.readableBytes();
                if (rb > 8) {
                    if (!init){
                        CharSequence charSequence = byteBuf.readCharSequence(rb, Charset.defaultCharset());
                        JSONObject jsonObject = JSON.parseObject(charSequence.toString());
                        if ("StartProxy".equals(jsonObject.get("Type"))) {
                            logger.info("=====StartProxy=====");
                            Bootstrap b = new Bootstrap();
                            b.group(group)
                                    .channel(NioSocketChannel.class)
                                    .option(ChannelOption.TCP_NODELAY, true)
                                    .handler(new ChannelInitializer<SocketChannel>() {
                                        protected void initChannel(SocketChannel ch) {
                                            ChannelPipeline p = ch.pipeline();
                                            //传入当前channel,用于将数据写回给ngrok server
                                            p.addLast(new FetchDataHandler(ctx.channel()));
                                        }
                                    });
                            //连接本地服务
                            f = b.connect("127.0.0.1", NgrokClient.LOCAL_PORT).sync();
                            logger.info("connect local port:"+f.channel().localAddress());
                            f.channel().closeFuture().addListener((ChannelFutureListener) t -> {
                                logger.info("disconnect local port:"+f.channel().localAddress());
                                init = false;
                            });
                            init = true;
                        }
                    }else {
                        //将用户请求数据转发给本地服务
                        logger.info("ProxyHandler write message to local port "+f.channel().localAddress()+":"+byteBuf.toString((CharsetUtil.UTF_8)));
                        f.channel().writeAndFlush(byteBuf.copy());
    
                    }
                }
            }
        }
    

    最后一步是ngrok连接本地服务后,完成的工作:

        private Channel channel;
        //传入连接到 ngrok server的channel
        FetchDataHandler(Channel channel) {
            this.channel=channel;
        }
    
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
            //将本地服务的数据写回给ngrok server的channel
            logger.info("FatchDataHandler write message to remote address " +channel.remoteAddress()+":"+ byteBuf.toString(CharsetUtil.UTF_8));
            channel.writeAndFlush(byteBuf.copy());
        }
    

    以上便是ngrok client 的netty实现过程。
    源码可前往我的github查看:Ngrok Client Java.

    相关文章

      网友评论

        本文标题:用Netty实现Ngrok Client【原创】

        本文链接:https://www.haomeiwen.com/subject/fcwpcctx.html