Go语言网络编程:Go实现简单的内网穿透

一、内网穿透

1. 内网穿透介绍

内网穿透是一种通过互联网建立远程连接到私有网络(内网)中设备的技术。通常情况下,内网设备由于位于一个受限制的网络环境中,无法直接从外部访问。内网穿透通过将内网设备的流量转发到公共网络上的服务器,然后再将该流量转发到客户端,从而实现与内网设备的通信。

2.内网穿透下访问流程

假设目前已经实现了内网穿透,以下从用户角度出发,给出了用户访问穿透后的公网网络的步骤

  1. 用户发出请求:用户通过公网地址发出一个访问请求,目标是服务端穿透后的公网地址。
  2. 服务端接收请求:服务端监听公网地址,接收到用户的请求后,将请求信息发送给客户端,如一个"New Connection"的消息。
  3. 客户端接收请求:客户端在内网中,持续监听来自服务端的信息。当接收到"New Connection"的消息后,知道有新的请求需要处理。
  4. 客户端连接隧道服务器:客户端接收到新的请求后,会创建一个新的连接到隧道服务器。隧道服务器在公网上,是客户端和服务端之间的桥梁。
  5. 客户端连接内网服务:同时,客户端也会创建一个新的连接到内网中的目标服务。这个服务是用户最初想要访问的服务。
  6. 数据转发:客户端将内网服务的数据拷贝到隧道服务器(如io.Copy(tunnelConn, clientConn)),同时将隧道服务器的数据拷贝到内网服务(io.Copy(clientConn, tunnelConn))。这样,用户的请求就通过隧道服务器,被转发到了内网服务。
  7. 服务端接收隧道数据:服务端在公网上,监听来自隧道服务器的连接。当隧道服务器有新的数据时,服务端会接收这些数据。
  8. 服务端转发数据:服务端接收到隧道服务器的数据后,会将这些数据转发给公网地址。可以通过io.Copy(appConn, tunnelConn)io.Copy(tunnelConn, appConn)实现,这两个函数将隧道服务器的数据拷贝到用户,同时将用户的数据拷贝到隧道服务器。
  9. 用户接收数据:用户最终接收到来自服务端的数据,这些数据实际上是内网服务的响应。这样,用户就成功地通过公网访问了内网服务。

以上就是用户发出访问请求到服务端、隧道、客户端的详细过程。通过这个过程,我们可以看到内网穿透是如何工作的。

二、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: 服务端监听地址。用于接收客户端的数据,然后转发给服务端本地服务。由ServerIPServerPort组合而成。
  • TunnelAddr: 隧道地址。用于客户端和服务端建立隧道通信,客户端和服务端通过该地址进行交互。由ServerIPTunnelPort组合而成。

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
}

这段代码是项目中常用的辅助函数包,它提供了一些用于网络通信的帮助函数:

  1. CreateListen(listenAddr string) (*net.TCPListener, error):该函数用于创建一个TCP监听器。它接受一个监听地址listenAddr作为参数,并通过net.ResolveTCPAddr解析地址,然后通过net.ListenTCP监听指定的端口。函数返回一个*net.TCPListener类型的指针和一个错误。
  2. CreateConnect(connectAddr string) (*net.TCPConn, error):该函数用于创建一个TCP连接。它接受一个服务端地址connectAddr作为参数,并通过net.ResolveTCPAddr解析地址,然后通过net.DialTCP与服务端建立连接。函数返回一个*net.TCPConn类型的指针和一个错误
  3. KeepAlive(conn *net.TCPConn):该函数用于保持TCP连接的活跃状态。通过循环向连接中写入数据,保持连接,每隔3秒写入一次,如果写入失败,说明连接已经断开,退出循环
  4. 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)
}

  1. main()函数:该函数是程序的入口点。它首先通过helper.CreateConnect函数连接到服务端,并打印连接地址。然后进入一个无限循环,从连接中读取数据。如果读取到的数据是"New Connection",则调用messgaeForward()函数连接隧道服务器进行消息转发。
  2. messgaeForward()函数:该函数用于连接隧道服务器并进行消息转发。它首先通过helper.CreateConnect函数分别连接到隧道服务器和客户端服务。然后使用io.Copy函数将客户端的数据拷贝到隧道服务器,将隧道服务器的数据拷贝到客户端。这样可以实现消息的双向转发。
    内网穿透的实现主要涉及到两个部分:客户端(Client)和服务端(Server)。下面我们将从代码角度出发,详细解释这两部分如何实现内网穿透。

客户端的主要任务是连接到服务端,并将内网服务的数据发送到服务端。这是通过以下关键步骤实现的:

  1. 连接到服务端:客户端首先需要连接到服务端。这是通过helper.CreateConnect(constant.ServerAddr)函数实现的,该函数创建一个到服务端的TCP连接。
  2. 读取服务端数据:客户端连接到服务端后,需要读取服务端发送的数据。这是通过helper.ReadMessage(serverConn)函数实现的,该函数从服务端连接中读取数据。
  3. 处理服务端数据:客户端读取到服务端数据后,需要根据数据内容进行相应的处理。如果读取到的数据是"New Connection",则需要连接到隧道服务器进行消息转发。
  4. 消息转发:消息转发是通过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())
		}
	}
}

  1. main()函数:该函数是程序的入口点。它启动了三个协程,分别用于监听服务端、目的服务端和隧道服务端。然后使用sync.WaitGroup等待所有协程结束。
  2. serverListen()函数:该函数用于监听客户端连接。它首先通过helper.CreateListen函数创建一个TCP监听器,并打印监听地址。然后进入一个无限循环,接收客户端连接。每当有新的客户端连接时,将其保持活跃,并启动一个新的协程进行处理。
  3. appListen()函数:该函数用于监听目的服务端连接。它首先通过helper.CreateListen函数创建一个TCP监听器,并打印监听地址。然后进入一个无限循环,接收目的服务端连接。每当有新的目的服务端连接时,向客户端发送"New Connection"的消息。
  4. tunnelListen()函数:该函数用于监听隧道客户端连接。它首先通过helper.CreateListen函数创建一个TCP监听器,并打印监听地址。然后进入一个无限循环,接收隧道客户端连接。每当有新的隧道客户端连接时,启动两个新的协程,分别将数据从隧道客户端拷贝到目的服务端,以及将数据从目的服务端拷贝到隧道客户端。

服务端的主要任务是监听客户端连接、目的服务端连接和隧道客户端连接,并进行数据转发。这是通过以下关键步骤实现的:

  1. 监听客户端连接:服务端首先需要监听客户端连接。这是通过helper.CreateListen(constant.ServerListenAddr)函数实现的,该函数创建一个监听客户端连接的TCP监听器。
  2. 处理客户端连接:服务端接收到客户端连接后,需要保持连接。这是通过helper.KeepAlive(serverConn)函数实现的,该函数保持与客户端的连接。
  3. 监听目的服务端连接:服务端需要监听目的服务端连接。这是通过helper.CreateListen(constant.AppTargetPort)函数实现的,该函数创建一个监听目的服务端连接的TCP监听器。
  4. 处理目的服务端连接:服务端接收到目的服务端连接后,需要向客户端发送"New Connection"的消息。这是通过serverConn.Write([]byte("New Connection"))实现的。
  5. 监听隧道客户端连接:服务端需要监听隧道客户端连接。这是通过helper.CreateListen(constant.TunnelListenAddr)函数实现的,该函数创建一个监听隧道客户端连接的TCP监听器。
  6. 数据转发:服务端接收到隧道客户端连接后,需要进行数据转发。这是通过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均可发现应用服务

扫描二维码关注公众号,回复: 15612126 查看本文章

在这里插入图片描述

6.2 公网ip测试步骤:以Linux云服务器为例

  1. 首先修改constant.go中的ServerIP为自己的公网ip
  2. 将服务端main.go打包,上传到Linux云服务器,并添加权限。windows下将go程序打包为linux可执行程序需要配置go编译环境,具体参考windows下将go程序打包为linux可执行程序教程
  3. 在服务器运行上传的main
  4. 在客户端运行客户端main.go
  5. 运行应用端main.go
  6. 此时可打开公网ip:AppTargetPort可发现内网的应用服务

猜你喜欢

转载自blog.csdn.net/qq_45808700/article/details/131417192
今日推荐