基于JAVA的内网穿透工具的实现

基于JAVA的内网穿透工具的实现

内网穿透的说明

内网穿透,即NAT穿透,网络连接时术语,计算机是局域网内时,外网与内网的计算机节点需要连接通信,有时就会出现不支持内网穿透。就是说映射端口,能让外网的电脑找到处于内网的电脑。不管是内网穿透还是其他类型的网络穿透,都是网络穿透的统一方法来研究和解决。

NAT技术

NAT技术是通过将专用的网络地址转换为公用地址,从而对外隐藏了内部管理的IP地址。这样,通过在内部使用非注册的IP地址,并将它们转换为一小部分外部注册的IP地址,从而减少了IP地址注册的费用以及节省了越来越缺乏的地址空间。同时,这也隐藏了内部网络结构,从而降低了内部网络受到攻击的风险。NAT的功能就是在内部网络的私有地址需要与外部通信时,把内部私有地址转换成合法的全局IP地址。NAT可以在两个方向上隐藏地址,为了支持这种方案,NAT在两个方向上都要翻译原地址和目的地址。NAT的功能通常被集成到路由器、防火墙等设备中。NAT设备维护一个映射表,用它来实现全局到本地和本地到全局的地址转换 [2] 。
NAT有三种类型:静态NAT(StaticNAT)、动态地址NAT(PooledNAT)、网络地址端口转换NAPT(Port-LevelNAT)。
其中,网络地址端口转换NAPT(NetworkAddressPortTranslation)则是把内部地址映射到外部网络的一个IP地址的不同端口上。它可以将中小型的网络隐藏在一个合法的IP地址后面。NAPT与动态地址NAT不同,它将内部连接映射到外部网络中的一个单独的IP地址上,同时在该地址上加上一个由NAT设备选定的端口号。
NAPT是使用最普遍的一种转换方式,在HomeGW中也主要使用该方式。它又包含两种转换方式:SNAT和DNAT。
(1)源NAT(SourceNAT,SNAT):修改数据包的源地址。源NAT改变第一个数据包的来源地址,它永远会在数据包发送到网络之前完成,数据包伪装就是一具SNAT的例子。
(2)目的NAT(DestinationNAT,DNAT):修改数据包的目的地址。DestinationNAT刚好与SNAT相反,它是改变第一个数据懈的目的地地址,如平衡负载、端口转发和透明代理就是属于DNAT。

SOCKET介绍

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
内容过多,这里不在过多的赘述,《Socket的学习(一)什么是Socket?》这篇有很全面的介绍,包括TCP/IP网络协议的介绍,这也是实现网络穿透的一个保证,即不考虑网络连接情况将数据内容进行完全转发,协议的解析等工作与直连方式不变。 在不考虑网络连接带来的IP变化的情况,可以将内容进行完全转发,协议的解析等工作与直连方式不变;也可以将协议通过解析后,再根据目标的协议进行组装,如HTTPS协议转HTTPS协议。

穿透思路

因NAT网内CLIENT可以正常连接到SERVER端,并且能够保持一段时间的长连接,则由CLIENT发起连接,建立SOCKET对,在SERVER收到外部请求时,可以通过已经建立好的SOCKET将数据传输给CLIENT,CLIENT使用相同的方式将数据发送给指定的网络程序,网络程序回发数据后则按原路返回给请求方。

内网服务 穿透内网端 穿透外网端 外部网络请求 (交互线程)发起socket连接作为两者交互通道 (外部连接)发起连接请求 利用(交互线程)通知有新的请求 (内网隧道)请求建立内网隧道 (内网隧道)建立成功 (外网隧道)建立新的SOCKET作为外网隧道 (外网隧道)建立成功 (外部连接)< - >(外网隧道)读写流成对建立 (外网隧道)< - >(内网隧道)读写流成对建立 (发送数据)使用(外部连接)发送数据 (发送数据)使用(外部隧道)发送数据 (发送数据)使用(内部隧道)发送数据 (回发数据)使用(内部隧道)回发数据 (回发数据)使用(外部隧道)回发数据 (回发数据)使用(外部连接)回发数据 内网服务 穿透内网端 穿透外网端 外部网络请求 内网穿透时序图

开始前我们先约定下词语:“客户端”:“穿透内网端”,“服务端”:“穿透外网端”,“应用”:“内网服务”,“请求”:“外部网络请求”。

从以上时序图可以看出,首先要使用一台外部网络的服务器(竟然有了外部网络,干嘛还要使用内网做事情?这个内网穿透主要是用来公司、团队形式的开发测试使用,当然也可以将自己内网的机器映射到外网,只要每月几十块就可租台自己的服务器,比如 “广告位招租” 云)。

内网需要一个客户端来提供隧道的打通工作,服务端无法直接向客户端发起连接,那么服务端怎么通知客户端?则就需要客户端提前建立一个长连接的socket对(交互线程),那么在有新连接请求时则可以通过交互线程通知客户端有新连接过来;则客户端发起一个新的socket连接,服务端收到新建立的socket后则将这个socket与请求的连接按“读对写、写对读”的方式建立隧道,客户端使用相同的方式将新的socket连接与内网连接建立隧道。

以上就绪后则可以将“请求”发来的数据通过两端隧道发送到应用上,应用收到数据则作出相应的返回;这个过程便可以完成一个完整的一问一答的请求(典型案例HTTP请求)了,那么再将这个隧道维持一个持久性,则可以做到外部请求到应用的长连接了(比如HTTP1.1、数据库连接等)。到这里便可以看出来,这个是固定端口的方式,那么像FTP这样的动态数据端口该怎么解决呢?我不知道 >_>。

在开发过程中有几个遇到的问题,在这里直接抛出来分享下,如果有更好的解决方案请留言:

  1. 新建立socket后,如何将连接配对?在新请求过来后,服务端向客户端发送通知携带一个格式化的不同串,根据字符串则可以获悉要对应哪个请求,从而建立隧道;但是现在的问题是在客户端不能向应用建立socket的时候,则按异常处理,不应向服务端建立连接,那么服务端则一直在等待状态,针对这个等待做的是线程进行定时清理超时连接。 定时清理很有效果。
  2. 在隧道出现异常(即网络断开或请求主动断开)的时候需要向上通知异常退出,当前用的是将自身作为参数传入,需要退出时子调用父函数,进行通知关闭,这样耦合太高了,如果使用control的形式,那么子存的参数更加的多,聚合度又差点。 用通知接口类的方式很香。
  3. 隧道传输过程的加密没有实现。 natcross2实现了。
  4. 自身HTTPS的协议方式没有实现,当前的做法是结合nginx的方式将HTTPS协议转换成HTTP协议来进行传输。 使用SSLServerSocket实现了HTTPS协议的传输,当然只是接收的时候是HTTPS,向目标传输时还是HTTP协议,换句话说就是隧道将SSL的外壳去掉了
  5. 总有一些莫名的连接持续存在,也不报错退出,即时请求端、应用端的机器都关掉。 退出时中间有报错而终止了线程,导致没有正常关闭端口,出问题的地方已经使用捕获异常处理了。

代码

本着 “Talk is cheap. Show me the code.” 原则,我们直接上代码。

  1. https://github.com/Pluto-Whong/natcross.git 不再维护了,建议使用natcross2
  2. https://github.com/Pluto-Whong/natcross-boot.git
  3. https://github.com/Pluto-Whong/natcross-boot-client.git

natcross2(https://github.com/Pluto-Whong/natcross2.git),这是核心码的实现,ServerApp运行在服务端,ClientApp运行在内网,便可以进行了,里面有个无加密的配置方式、交互加密、交互和隧道加密的方式,可以根据需要选择运行。

感兴趣的同学只选择natcross2进行使用即可,natcross-boot、natcross-boot-client是一种快捷的封装,主要是为团队方便管理写的管理程序,不用细研究,所有值得看的都在natcross2中了

以上三个分别是核心码、共用的服务端、共用的客户端。
核心码即主要代码,这个库已经可以做到内网穿透的功能业务,ServerApp运行在服务端,ClientApp运行在内网,便可以进行了,这个核心码已经支持了多端口的监听,客户端创建多个ClientControlThread便可以实现多映射的穿透。
公用服务端,是针对公司、团队的环境进行的WEB管理方式的封装,使用spring-boot作为框架进行部署,登录后便可以自由添加修改端口情况。使用sqlite进行数据持久化。

初始化密码,用户名:admin 密码:123456
web管理方式

web管理方式

共用客户端,则是和公用服务端配套的设置,可以将客户端部署到一台内网机器上,客户端会定时获取服务端的设置来建立自己的客户端线程,从而实现自由定制化。
  • 29
    点赞
  • 118
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 45
    评论
好的,关于基于Java写一个工具实现内网穿透的问题,我可以回答。 内网穿透是指通过一个公网服务器,将内网中的服务暴露到公网上,使得公网用户可以访问到内网中的服务。常见的应用场景有开发环境的远程调试、内网访问外部服务等。下面是一个基于Java实现内网穿透的简单工具类: ```java import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class PortForwarding { public static void main(String[] args) throws IOException { int localPort = Integer.parseInt(args[0]); // 本地端口 String remoteHost = args[1]; // 远程主机 int remotePort = Integer.parseInt(args[2]); // 远程端口 ServerSocket serverSocket = new ServerSocket(localPort); System.out.println("本地监听端口:" + localPort); while (true) { Socket localSocket = serverSocket.accept(); System.out.println("收到本地请求:" + localSocket); Socket remoteSocket = new Socket(remoteHost, remotePort); System.out.println("连接远程主机:" + remoteHost + ":" + remotePort); Thread t1 = new Thread(() -> { try { forward(localSocket.getInputStream(), remoteSocket.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } }); Thread t2 = new Thread(() -> { try { forward(remoteSocket.getInputStream(), localSocket.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } }); t1.start(); t2.start(); } } private static void forward(java.io.InputStream inputStream, java.io.OutputStream outputStream) throws IOException { byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, len); outputStream.flush(); } outputStream.close(); } } ``` 使用方法如下: 1. 在公网服务器上启动该程序,监听一个公网端口(如8000): ```bash java PortForwarding 8000 127.0.0.1 80 ``` 2. 在内网机器上,将需要穿透到公网的服务端口(如80)映射到公网服务器上的公网端口(如8000): ```bash ssh -NR 8000:localhost:80 user@public_server ``` 这样,就可以通过访问公网服务器的8000端口,访问到内网机器上的80端口了。 注意,以上仅仅是一个简单的内网穿透实现,实际应用中还需要考虑一些安全性问题,如身份认证、数据加密等。
评论 45
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

plutoppppp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值