一、内网穿透
1. 内网穿透介绍
内网穿透是一种通过互联网建立远程连接到私有网络(内网)中设备的技术。通常情况下,内网设备由于位于一个受限制的网络环境中,无法直接从外部访问。内网穿透通过将内网设备的流量转发到公共网络上的服务器,然后再将该流量转发到客户端,从而实现与内网设备的通信。
2.内网穿透下访问流程
假设目前已经实现了内网穿透,以下从用户角度出发,给出了用户访问穿透后的公网网络的步骤
- 用户发出请求:用户通过公网地址发出一个访问请求,目标是服务端穿透后的公网地址。
- 服务端接收请求:服务端监听公网地址,接收到用户的请求后,将请求信息发送给客户端,如一个"New Connection"的消息。
- 客户端接收请求:客户端在内网中,持续监听来自服务端的信息。当接收到"New Connection"的消息后,知道有新的请求需要处理。
- 客户端连接隧道服务器:客户端接收到新的请求后,会创建一个新的连接到隧道服务器。隧道服务器在公网上,是客户端和服务端之间的桥梁。
- 客户端连接内网服务:同时,客户端也会创建一个新的连接到内网中的目标服务。这个服务是用户最初想要访问的服务。
- 数据转发:客户端将内网服务的数据拷贝到隧道服务器(如
io.Copy(tunnelConn, clientConn)
),同时将隧道服务器的数据拷贝到内网服务(io.Copy(clientConn, tunnelConn)
)。这样,用户的请求就通过隧道服务器,被转发到了内网服务。 - 服务端接收隧道数据:服务端在公网上,监听来自隧道服务器的连接。当隧道服务器有新的数据时,服务端会接收这些数据。
- 服务端转发数据:服务端接收到隧道服务器的数据后,会将这些数据转发给公网地址。可以通过
io.Copy(appConn, tunnelConn)
和io.Copy(tunnelConn, appConn)
实现,这两个函数将隧道服务器的数据拷贝到用户,同时将用户的数据拷贝到隧道服务器。 - 用户接收数据:用户最终接收到来自服务端的数据,这些数据实际上是内网服务的响应。这样,用户就成功地通过公网访问了内网服务。
以上就是用户发出访问请求到服务端、隧道、客户端的详细过程。通过这个过程,我们可以看到内网穿透是如何工作的。
二、go实现简单的内网穿透
实现内网穿透需要两个部分:客户端和服务端。客户端运行在内网中,服务端运行在公网中。客户端和服务端通过隧道进行通信,客户端将内网服务的数据发送到服务端,服务端将公网的请求发送到客户端。
要实现内网穿透,通常需要以下几个步骤:
- 服务器端:在公网上搭建一个可被访问的服务器,用于接收外部连接和内网设备的转发流量,当然若没有公网ip,也可以使用本地的端口进行模拟
- 客户端:在内网设备所在的计算机上运行一个客户端程序,负责将内网设备的流量转发到服务器。
- 端口映射:将服务器上的某个端口与内网设备的指定端口进行映射,使得外部可以通过这个端口访问内网设备。
- 数据传输:服务器接收到来自客户端的数据后,将其转发给内网设备,同时将内网设备的响应再转发回客户端。
0. 项目源码地址:https://github.com/Pistachiout/Net-Penetration
1. constant.go常量端口定义
package constant
const (
// 需要内网穿透的服务端口,运行内网穿透客户端后,该端口将映射到公网,公网可以访问该端口
AppPort = ":4000"
// ServerPort 服务端监听端口,用于连接客户端,一般为公网端口,服务端监听该端口,接收客户端的数据,然后转发给服务端本地服务
ServerPort = ":8081"
// TunnelPort 隧道端口,用于客户端和服务端建立隧道,客户端和服务端通过该端口通信
TunnelPort = ":8082"
// AppTargetPort 穿透后的目的服务端口,在服务端端口运行客户端应用
AppTargetPort = ":8083"
// ServerIP 服务端公网ip地址,用于连接服务端,一般为公网IP
ServerIP = ""
// ServerAddr 服务端监听地址,用于接收客户端的数据,然后转发给服务端本地服务
ServerAddr = ServerIP + ServerPort
// TunnelAddr 隧道地址,用于客户端和服务端建立隧道,客户端和服务端通过该地址通信
TunnelAddr = ServerIP + TunnelPort
// BufSize 缓冲区大小
BufSize = 1024
)
以上常量定义了在内网穿透过程中所需的各种端口和地址。
AppPort
: 内网端口,需要内网穿透的应用端口,如web应用。当运行内网穿透客户端后,该端口将被映射到公网,使得通过公网可以访问该端口对应的服务。ServerPort
: 服务端监听端口。服务端监听该端口,接收客户端的数据,发给服务端本地服务。TunnelPort
: 隧道端口。用于客户端和服务端建立隧道通信,客户端和服务端通过该端口进行交互。AppTargetPort
: 穿透后的目标服务端口。在服务端端口运行客户端应用,用户可通过访问公网ip的该端口访问内网服务BufSize
: 缓冲区大小。定义了在数据传输过程中所使用的缓冲区大小,一般为1024字节。
这些常量提供了在内网穿透过程中所需的各种配置信息,可以根据具体的应场景和需求进行相应的调整和配置。若用户拥有公网ip,还需要添加公网ip地址
ServerIP
: 服务端公网IP地址。用于连接服务端,一般为公网IP,用户可根据自己的公网ip进行设置。客户端需要根据这个IP来与服务端建立连接。ServerAddr
: 服务端监听地址。用于接收客户端的数据,然后转发给服务端本地服务。由ServerIP
和ServerPort
组合而成。TunnelAddr
: 隧道地址。用于客户端和服务端建立隧道通信,客户端和服务端通过该地址进行交互。由ServerIP
和TunnelPort
组合而成。
2. 辅助函数包helper.go
package helper
import (
"log"
"net"
"time"
)
// CreateListen 监听,参数为监听地址listenAddr,返回 TCPListener,通过 net.ResolveTCPAddr 解析地址,通过 net.ListenTCP 监听端口
//
// 监听是指服务端监听某个端口,等待客户端的连接,一旦客户端连接上来,服务端就会创建一个新的goroutine处理客户端的请求。
//
// ResolveTCPAddr是一个解析TCP地址的函数,addr为域名或者IP地址加端口号,返回一个TCPAddr,该结构体包含了ip和port
// ListenTCP函数监听TCP地址,addr则是一个TCP地址,返回值l是一个net.Listener接口,可以用来接收连接。
func CreateListen(listenAddr string) (*net.TCPListener, error) {
tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr)
if err != nil {
return nil, err
}
tcpListener, err := net.ListenTCP("tcp", tcpAddr)
return tcpListener, err
}
// CreateConnect 连接,参数为服务端地址connectAddr,返回 TCPConn,通过 net.ResolveTCPAddr 解析地址,通过 net.DialTCP 连接服务端
// 连接是指客户端连接服务端,连接成功后,客户端就可以向服务端发送数据了,与监听不同的是,连接是客户端发起的,而监听是服务端发起的。
// DialTCP函数在网络协议tcp上连接本地地址laddr和远端地址raddr
func CreateConnect(connectAddr string) (*net.TCPConn, error) {
// 解析地址,返回TCPAddr
tcpAddr, err := net.ResolveTCPAddr("tcp", connectAddr)
if err != nil {
return nil, err
}
tcpConn, err := net.DialTCP("tcp", nil, tcpAddr)
return tcpConn, err
}
// KeepAlive 保持连接,参数为连接conn,通过循环向连接中写入数据,保持连接,每隔3秒写入一次,如果写入失败,说明连接已经断开,退出循环
func KeepAlive(conn *net.TCPConn) {
for {
_, err := conn.Write([]byte("KeepAlive"))
if err != nil {
log.Printf("[KeepAlive] Error %s", err)
return
}
time.Sleep(time.Second * 3)
}
}
// GetDataFromConnection for循环获取Connection中的数据
func GetDataFromConnection(bufSize int, conn *net.TCPConn) ([]byte, error) {
b := make([]byte, 0)
for {
// 读取数据
data := make([]byte, bufSize)
n, err := conn.Read(data)
if err != nil {
return nil, err
}
b = append(b, data[:n]...)
if n < bufSize {
break
}
}
return b, nil
}
这段代码是项目中常用的辅助函数包,它提供了一些用于网络通信的帮助函数:
CreateListen(listenAddr string) (*net.TCPListener, error)
:该函数用于创建一个TCP监听器。它接受一个监听地址listenAddr
作为参数,并通过net.ResolveTCPAddr
解析地址,然后通过net.ListenTCP
监听指定的端口。函数返回一个*net.TCPListener
类型的指针和一个错误。CreateConnect(connectAddr string) (*net.TCPConn, error)
:该函数用于创建一个TCP连接。它接受一个服务端地址connectAddr
作为参数,并通过net.ResolveTCPAddr
解析地址,然后通过net.DialTCP
与服务端建立连接。函数返回一个*net.TCPConn
类型的指针和一个错误KeepAlive(conn *net.TCPConn)
:该函数用于保持TCP连接的活跃状态。通过循环向连接中写入数据,保持连接,每隔3秒写入一次,如果写入失败,说明连接已经断开,退出循环GetDataFromConnection(bufSize int, conn *net.TCPConn) ([]byte, error)
:该函数用于从TCP连接中获取数据。它接受一个缓冲区大小bufSize
和一个*net.TCPConn
类型的指针作为参数。函数通过循环从连接中读取数据,并将读取的数据追加到一个字节数组中,直到读取完所有数据或达到缓冲区大小。函数返回读取到的数据字节数组和一个错误。
3. 客户端main.go
package main
import (
"Net-Penetration/constant"
"Net-Penetration/helper"
"io"
"log"
)
// 内网穿透客户端,用于将本地服务映射到公网,使得公网可以访问本地服务,实现内网穿透,
func main() {
// 连接服务端
conn, err := helper.CreateConnect(constant.ServerAddr)
if err != nil {
panic(err)
}
log.Printf("连接成功,连接地址为:%s\n", conn.RemoteAddr().String())
// 保持连接,读取数据
for {
// 从连接中读取数据
data, err := helper.GetDataFromConnection(constant.BufSize, conn)
if err != nil {
log.Printf("读取数据失败,错误信息为:%s\n", err.Error())
continue
}
log.Printf("接收到数据:%s\n", string(data))
// 判断是否为新连接,如果是新连接,则连接隧道服务器,否则转发消息
if string(data) == "New Connection" {
// 连接隧道服务器
go messgaeForward()
}
}
}
// 连接隧道服务器进行消息转发
func messgaeForward() {
// 连接隧道服务器
tunnelConn, err := helper.CreateConnect(constant.TunnelAddr)
if err != nil {
panic(err)
}
// 连接客户端服务
clientConn, err := helper.CreateConnect(constant.AppPort)
if err != nil {
panic(err)
}
// 消息转发
// io.Copy()函数实现了数据的拷贝,可以将数据从一个接口拷贝到另一个接口,这里将客户端的数据拷贝到隧道服务器,将隧道服务器的数据拷贝到客户端
// io.Copy()函数会一直阻塞,直到两个接口中的数据全部拷贝完成
go io.Copy(clientConn, tunnelConn)
go io.Copy(tunnelConn, clientConn)
}
main()
函数:该函数是程序的入口点。它首先通过helper.CreateConnect
函数连接到服务端,并打印连接地址。然后进入一个无限循环,从连接中读取数据。如果读取到的数据是"New Connection",则调用messgaeForward()
函数连接隧道服务器进行消息转发。messgaeForward()
函数:该函数用于连接隧道服务器并进行消息转发。它首先通过helper.CreateConnect
函数分别连接到隧道服务器和客户端服务。然后使用io.Copy
函数将客户端的数据拷贝到隧道服务器,将隧道服务器的数据拷贝到客户端。这样可以实现消息的双向转发。
内网穿透的实现主要涉及到两个部分:客户端(Client)和服务端(Server)。下面我们将从代码角度出发,详细解释这两部分如何实现内网穿透。
客户端的主要任务是连接到服务端,并将内网服务的数据发送到服务端。这是通过以下关键步骤实现的:
- 连接到服务端:客户端首先需要连接到服务端。这是通过
helper.CreateConnect(constant.ServerAddr)
函数实现的,该函数创建一个到服务端的TCP连接。 - 读取服务端数据:客户端连接到服务端后,需要读取服务端发送的数据。这是通过
helper.ReadMessage(serverConn)
函数实现的,该函数从服务端连接中读取数据。 - 处理服务端数据:客户端读取到服务端数据后,需要根据数据内容进行相应的处理。如果读取到的数据是"New Connection",则需要连接到隧道服务器进行消息转发。
- 消息转发:消息转发是通过
messgaeForward()
函数实现的。该函数首先连接到隧道服务器和客户端服务,然后将客户端服务的数据拷贝到隧道服务器,同时将隧道服务器的数据拷贝到客户端服务。
4.服务端main.go
package main
import (
"Net-Penetration/constant"
"Net-Penetration/helper"
"io"
"log"
"net"
"sync"
)
// serverConn 服务端连接
var serverConn *net.TCPConn
// appConn 目的服务端连接
var appConn *net.TCPConn
// wg 用于等待所有协程结束
var wg sync.WaitGroup
// 内网穿透服务端
func main() {
//监听服务端
go serverListen()
//监听目的服务端
go appListen()
//启动隧道服务
go tunnelListen()
//等待所有协程结束
wg.Add(1)
wg.Wait()
}
func serverListen() {
//监听服务端,用于接收客户端连接
tcpListener, err := helper.CreateListen(constant.ServerAddr)
if err != nil {
panic(err)
}
log.Printf("服务端监听地址为:%s\n", tcpListener.Addr().String())
//接收客户端连接
for {
serverConn, err = tcpListener.AcceptTCP()
if err != nil {
log.Printf("接收连接失败,错误信息为:%s\n", err.Error())
return
}
//保持连接
go helper.KeepAlive(serverConn)
}
}
// 监听隧道服务,用于接收隧道客户端连接,隧道客户端连接用于转发目的服务端和客户端之间的消息,实现内网穿透
func tunnelListen() {
tcpListener, err := helper.CreateListen(constant.TunnelAddr)
if err != nil {
panic(err)
}
log.Printf("隧道监听地址为:%s\n", tcpListener.Addr().String())
for {
tunnelConn, err := tcpListener.AcceptTCP()
if err != nil {
log.Printf("接收连接失败,错误信息为:%s\n", err.Error())
return
}
// 数据转发
go io.Copy(appConn, tunnelConn)
go io.Copy(tunnelConn, appConn)
}
}
// 监听目的服务端,用于接收目的服务端连接,目的服务端可以是本地转发的端口,也可以是远程服务器的端口
func appListen() {
//监听目的服务端
tcpListener, err := helper.CreateListen(constant.AppTargetPort)
if err != nil {
panic(err)
}
log.Printf("应用目的服务端监端口地址为:%s\n", tcpListener.Addr().String())
for {
appConn, err = tcpListener.AcceptTCP()
if err != nil {
log.Printf("接收连接失败,错误信息为:%s\n", err.Error())
return
}
_, err := serverConn.Write([]byte("New Connection"))
if err != nil {
log.Printf("发送消息失败,错误信息为:%s\n", err.Error())
}
}
}
main()
函数:该函数是程序的入口点。它启动了三个协程,分别用于监听服务端、目的服务端和隧道服务端。然后使用sync.WaitGroup
等待所有协程结束。serverListen()
函数:该函数用于监听客户端连接。它首先通过helper.CreateListen
函数创建一个TCP监听器,并打印监听地址。然后进入一个无限循环,接收客户端连接。每当有新的客户端连接时,将其保持活跃,并启动一个新的协程进行处理。appListen()
函数:该函数用于监听目的服务端连接。它首先通过helper.CreateListen
函数创建一个TCP监听器,并打印监听地址。然后进入一个无限循环,接收目的服务端连接。每当有新的目的服务端连接时,向客户端发送"New Connection"的消息。tunnelListen()
函数:该函数用于监听隧道客户端连接。它首先通过helper.CreateListen
函数创建一个TCP监听器,并打印监听地址。然后进入一个无限循环,接收隧道客户端连接。每当有新的隧道客户端连接时,启动两个新的协程,分别将数据从隧道客户端拷贝到目的服务端,以及将数据从目的服务端拷贝到隧道客户端。
服务端的主要任务是监听客户端连接、目的服务端连接和隧道客户端连接,并进行数据转发。这是通过以下关键步骤实现的:
- 监听客户端连接:服务端首先需要监听客户端连接。这是通过
helper.CreateListen(constant.ServerListenAddr)
函数实现的,该函数创建一个监听客户端连接的TCP监听器。 - 处理客户端连接:服务端接收到客户端连接后,需要保持连接。这是通过
helper.KeepAlive(serverConn)
函数实现的,该函数保持与客户端的连接。 - 监听目的服务端连接:服务端需要监听目的服务端连接。这是通过
helper.CreateListen(constant.AppTargetPort)
函数实现的,该函数创建一个监听目的服务端连接的TCP监听器。 - 处理目的服务端连接:服务端接收到目的服务端连接后,需要向客户端发送"New Connection"的消息。这是通过
serverConn.Write([]byte("New Connection"))
实现的。 - 监听隧道客户端连接:服务端需要监听隧道客户端连接。这是通过
helper.CreateListen(constant.TunnelListenAddr)
函数实现的,该函数创建一个监听隧道客户端连接的TCP监听器。 - 数据转发:服务端接收到隧道客户端连接后,需要进行数据转发。这是通过
io.Copy(appConn, tunnelConn)
和io.Copy(tunnelConn, appConn)
实现的,这两个函数将隧道客户端的数据拷贝到目的服务端,同时将目的服务端的数据拷贝到隧道客户端。
5.应用端测试main.go
package main
import (
"Net-Penetration/constant"
"encoding/json"
"log"
"net/http"
)
// 本地应用,用于测试内网穿透
func main() {
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
q := request.URL.Query()
b, err := json.Marshal(q)
if err != nil {
log.Println(err)
}
writer.Write(b)
})
log.Printf("本地服务已启动:%s\n", constant.AppPort)
http.ListenAndServe(constant.AppPort, nil)
}
6.1 本地测试步骤:
1.首先运行服务端main.go
2. 运行客户端main.go
3. 运行应用端main.go
4. 此时可分别打开AppPort和AppTargetPort均可发现应用服务
6.2 公网ip测试步骤:以Linux云服务器为例
- 首先修改constant.go中的ServerIP为自己的公网ip
- 将服务端main.go打包,上传到Linux云服务器,并添加权限。windows下将go程序打包为linux可执行程序需要配置go编译环境,具体参考windows下将go程序打包为linux可执行程序教程:
- 在服务器运行上传的main
- 在客户端运行客户端main.go
- 运行应用端main.go
- 此时可打开公网ip:AppTargetPort可发现内网的应用服务